@shipfox/api-integration-slack 4.0.0 → 5.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 +24 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +3 -1
- package/dist/api/client.js.map +1 -1
- package/dist/core/webhook.d.ts +5 -3
- package/dist/core/webhook.d.ts.map +1 -1
- package/dist/core/webhook.js +101 -2
- package/dist/core/webhook.js.map +1 -1
- package/dist/db/db.d.ts +34 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/installations.d.ts +2 -0
- package/dist/db/installations.d.ts.map +1 -1
- package/dist/db/installations.js +4 -2
- package/dist/db/installations.js.map +1 -1
- package/dist/db/schema/installations.d.ts +17 -0
- package/dist/db/schema/installations.d.ts.map +1 -1
- package/dist/db/schema/installations.js +3 -1
- package/dist/db/schema/installations.js.map +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/routes/webhooks.d.ts +2 -1
- package/dist/presentation/routes/webhooks.d.ts.map +1 -1
- package/dist/presentation/routes/webhooks.js +1 -0
- package/dist/presentation/routes/webhooks.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0001_dark_tyrannus.sql +1 -0
- package/drizzle/meta/0001_snapshot.json +139 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +18 -28
- package/src/api/client.test.ts +24 -0
- package/src/api/client.ts +3 -1
- package/src/core/disconnect.test.ts +49 -0
- package/src/core/webhook.test.ts +131 -2
- package/src/core/webhook.ts +143 -6
- package/src/db/installations.test.ts +15 -0
- package/src/db/installations.ts +11 -3
- package/src/db/schema/installations.ts +3 -1
- package/src/index.test.ts +11 -0
- package/src/index.ts +11 -0
- package/src/presentation/routes/webhooks.test.ts +28 -1
- package/src/presentation/routes/webhooks.ts +3 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/turbo.json +9 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
$ shipfox-swc
|
|
2
|
-
Successfully compiled: 24 files with swc (
|
|
2
|
+
Successfully compiled: 24 files with swc (243.83ms)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @shipfox/api-integration-slack
|
|
2
2
|
|
|
3
|
+
## 5.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2875241: Adds deduplicated Slack installation revocation for app uninstall and bot token-revocation events.
|
|
8
|
+
- fb70438: Cascades provider installation and token deletion when removing a connection.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- bb037af: Resolves workspace packages from source during development while published consumers continue to use compiled output.
|
|
13
|
+
- Updated dependencies [2875241]
|
|
14
|
+
- Updated dependencies [bb037af]
|
|
15
|
+
- Updated dependencies [fb70438]
|
|
16
|
+
- @shipfox/api-integration-slack-dto@5.0.0
|
|
17
|
+
- @shipfox/api-integration-core-dto@5.0.0
|
|
18
|
+
- @shipfox/api-auth-context@5.0.0
|
|
19
|
+
- @shipfox/api-secrets@5.0.0
|
|
20
|
+
- @shipfox/api-workspaces@5.0.0
|
|
21
|
+
- @shipfox/config@1.2.2
|
|
22
|
+
- @shipfox/node-drizzle@0.3.1
|
|
23
|
+
- @shipfox/node-fastify@0.2.3
|
|
24
|
+
- @shipfox/node-opentelemetry@0.5.2
|
|
25
|
+
- @shipfox/node-postgres@0.4.2
|
|
26
|
+
|
|
3
27
|
## 4.0.0
|
|
4
28
|
|
|
5
29
|
### Minor Changes
|
package/dist/api/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,yBAAyB,CAAC,KAAK,EAAE;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC9E,WAAW,CAAC,KAAK,EAAE;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,UAAU,CAAC,KAAK,EAAE;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACpC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CAClC;AAeD,wBAAgB,oBAAoB,IAAI,cAAc,CAwCrD"}
|
package/dist/api/client.js
CHANGED
|
@@ -3,6 +3,7 @@ import ky, { HTTPError, TimeoutError } from 'ky';
|
|
|
3
3
|
import { config } from '#config.js';
|
|
4
4
|
import { SlackEnterpriseInstallUnsupportedError, SlackIntegrationProviderError, SlackTokenRotationUnsupportedError } from '#core/errors.js';
|
|
5
5
|
const SLACK_API_TIMEOUT_MS = 10_000;
|
|
6
|
+
const trailingSlashesPattern = /\/+$/;
|
|
6
7
|
export function createSlackApiClient() {
|
|
7
8
|
return {
|
|
8
9
|
async exchangeAuthorizationCode (input) {
|
|
@@ -151,7 +152,8 @@ function slackMethodArguments(input) {
|
|
|
151
152
|
return body;
|
|
152
153
|
}
|
|
153
154
|
function slackApiUrl(path) {
|
|
154
|
-
|
|
155
|
+
const baseUrl = config.SLACK_API_BASE_URL.replace(trailingSlashesPattern, '');
|
|
156
|
+
return new URL(path, `${baseUrl}/`).toString();
|
|
155
157
|
}
|
|
156
158
|
function retryAfterSeconds(headers) {
|
|
157
159
|
const retryAfter = headers.get('retry-after');
|
package/dist/api/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/api/client.ts"],"sourcesContent":["import {logger} from '@shipfox/node-opentelemetry';\nimport ky, {HTTPError, TimeoutError} from 'ky';\nimport {config} from '#config.js';\nimport {\n SlackEnterpriseInstallUnsupportedError,\n SlackIntegrationProviderError,\n SlackTokenRotationUnsupportedError,\n} from '#core/errors.js';\n\nconst SLACK_API_TIMEOUT_MS = 10_000;\n\nexport interface SlackAuthorization {\n accessToken: string;\n botUserId: string;\n appId: string;\n teamId: string;\n teamName: string;\n scopes: string[];\n}\n\nexport interface SlackWebApiResponse {\n ok: boolean;\n error?: string | undefined;\n [key: string]: unknown;\n}\n\nexport interface SlackApiClient {\n exchangeAuthorizationCode(input: {code: string}): Promise<SlackAuthorization>;\n revokeToken(input: {token: string}): Promise<void>;\n callMethod(input: {\n method: string;\n token: string;\n arguments: Record<string, unknown>;\n }): Promise<SlackWebApiResponse>;\n}\n\ninterface SlackOAuthAccessResponse {\n ok?: unknown;\n error?: unknown;\n access_token?: unknown;\n bot_user_id?: unknown;\n app_id?: unknown;\n team?: {id?: unknown; name?: unknown} | null | undefined;\n scope?: unknown;\n is_enterprise_install?: unknown;\n expires_in?: unknown;\n refresh_token?: unknown;\n}\n\nexport function createSlackApiClient(): SlackApiClient {\n return {\n async exchangeAuthorizationCode(input) {\n const body = await mapSlackError('exchange-authorization-code', () =>\n ky\n .post(slackApiUrl('oauth.v2.access'), {\n body: new URLSearchParams({\n client_id: config.SLACK_OAUTH_CLIENT_ID,\n client_secret: config.SLACK_OAUTH_CLIENT_SECRET,\n code: input.code,\n redirect_uri: config.SLACK_OAUTH_REDIRECT_URL,\n }),\n timeout: SLACK_API_TIMEOUT_MS,\n })\n .json<SlackOAuthAccessResponse>(),\n );\n return parseOAuthAccess(body);\n },\n\n async revokeToken(input) {\n await mapSlackError('revoke-token', async () => {\n await ky.post(slackApiUrl('auth.revoke'), {\n headers: {authorization: `Bearer ${input.token}`},\n timeout: SLACK_API_TIMEOUT_MS,\n });\n });\n },\n\n async callMethod(input) {\n return await mapSlackWebApiError(input.method, () =>\n ky\n .post(slackApiUrl(input.method), {\n headers: {authorization: `Bearer ${input.token}`},\n body: slackMethodArguments(input.arguments),\n timeout: SLACK_API_TIMEOUT_MS,\n })\n .json<SlackWebApiResponse>(),\n );\n },\n };\n}\n\nfunction parseOAuthAccess(body: SlackOAuthAccessResponse): SlackAuthorization {\n if (body.ok !== true) {\n throw new SlackIntegrationProviderError(\n slackOAuthErrorReason(body.error),\n 'Slack authorization request failed',\n );\n }\n if (body.is_enterprise_install === true || body.team == null) {\n throw new SlackEnterpriseInstallUnsupportedError();\n }\n // The bot-token store has no refresh path, so a rotating (expiring) token must never be persisted.\n // Slack omits both fields when rotation is off, so presence of either signals rotation.\n // The rejected token self-expires, matching the Enterprise Grid rejection path.\n if (body.expires_in !== undefined || body.refresh_token !== undefined) {\n throw new SlackTokenRotationUnsupportedError();\n }\n const {access_token: accessToken, bot_user_id: botUserId, app_id: appId, team, scope} = body;\n if (\n typeof accessToken !== 'string' ||\n typeof botUserId !== 'string' ||\n typeof appId !== 'string' ||\n typeof team.id !== 'string' ||\n typeof team.name !== 'string' ||\n typeof scope !== 'string'\n ) {\n throw new SlackIntegrationProviderError(\n 'malformed-provider-response',\n 'Slack authorization response did not include the required installation details',\n );\n }\n return {\n accessToken,\n botUserId,\n appId,\n teamId: team.id,\n teamName: team.name,\n scopes: scope\n .split(',')\n .map((value) => value.trim())\n .filter(Boolean),\n };\n}\n\nfunction slackOAuthErrorReason(error: unknown) {\n if (error === 'service_unavailable' || error === 'internal_error') return 'provider-unavailable';\n if (error === 'ratelimited') return 'rate-limited';\n return 'access-denied';\n}\n\nasync function mapSlackError<T>(operation: string, request: () => Promise<T>): Promise<T> {\n try {\n return await request();\n } catch (error) {\n if (error instanceof SlackIntegrationProviderError) throw error;\n if (error instanceof HTTPError) {\n const {status, statusText, headers} = error.response;\n logger().warn({operation, status, statusText}, 'Slack API request rejected');\n if (status === 429) {\n throw new SlackIntegrationProviderError(\n 'rate-limited',\n 'Slack request was rate limited',\n retryAfterSeconds(headers),\n );\n }\n if (status >= 500) {\n throw new SlackIntegrationProviderError('provider-unavailable', 'Slack request failed');\n }\n throw new SlackIntegrationProviderError('access-denied', 'Slack request was rejected');\n }\n if (error instanceof TimeoutError) {\n logger().warn({operation}, 'Slack API request timed out');\n throw new SlackIntegrationProviderError('timeout', 'Slack request timed out');\n }\n logger().warn(\n {operation, errName: error instanceof Error ? error.name : typeof error},\n 'Slack API request failed',\n );\n throw new SlackIntegrationProviderError('provider-unavailable', 'Slack request failed');\n }\n}\n\nasync function mapSlackWebApiError<T>(method: string, request: () => Promise<T>): Promise<T> {\n try {\n return await request();\n } catch (error) {\n if (error instanceof SlackIntegrationProviderError) throw error;\n if (error instanceof HTTPError) {\n const {status, statusText, headers} = error.response;\n logger().warn(\n {operation: 'call-method', method, status, statusText},\n 'Slack API request rejected',\n );\n if (status === 429) {\n throw new SlackIntegrationProviderError(\n 'rate-limited',\n 'Slack request was rate limited',\n retryAfterSeconds(headers),\n );\n }\n if (status === 413) {\n throw new SlackIntegrationProviderError(\n 'content-too-large',\n 'Slack request content was too large',\n );\n }\n if (status >= 500) {\n throw new SlackIntegrationProviderError('provider-unavailable', 'Slack request failed');\n }\n throw new SlackIntegrationProviderError(\n 'malformed-provider-response',\n 'Slack request was rejected',\n );\n }\n if (error instanceof TimeoutError) {\n logger().warn({operation: 'call-method', method}, 'Slack API request timed out');\n throw new SlackIntegrationProviderError('timeout', 'Slack request timed out');\n }\n logger().warn(\n {\n operation: 'call-method',\n method,\n errName: error instanceof Error ? error.name : typeof error,\n },\n 'Slack API request failed',\n );\n throw new SlackIntegrationProviderError('provider-unavailable', 'Slack request failed');\n }\n}\n\nfunction slackMethodArguments(input: Record<string, unknown>): URLSearchParams {\n const body = new URLSearchParams();\n for (const [key, value] of Object.entries(input)) {\n if (value === null || value === undefined) continue;\n const serialized =\n typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'\n ? String(value)\n : JSON.stringify(value);\n if (serialized !== undefined) body.set(key, serialized);\n }\n return body;\n}\n\nfunction slackApiUrl(path: string): string {\n return new URL(path, `${config.SLACK_API_BASE_URL}/`).toString();\n}\n\nfunction retryAfterSeconds(headers: Headers): number | undefined {\n const retryAfter = headers.get('retry-after');\n if (!retryAfter) return undefined;\n const parsed = Number.parseInt(retryAfter, 10);\n return Number.isNaN(parsed) ? undefined : parsed;\n}\n"],"names":["logger","ky","HTTPError","TimeoutError","config","SlackEnterpriseInstallUnsupportedError","SlackIntegrationProviderError","SlackTokenRotationUnsupportedError","SLACK_API_TIMEOUT_MS","createSlackApiClient","exchangeAuthorizationCode","input","body","mapSlackError","post","slackApiUrl","URLSearchParams","client_id","SLACK_OAUTH_CLIENT_ID","client_secret","SLACK_OAUTH_CLIENT_SECRET","code","redirect_uri","SLACK_OAUTH_REDIRECT_URL","timeout","json","parseOAuthAccess","revokeToken","headers","authorization","token","callMethod","mapSlackWebApiError","method","slackMethodArguments","arguments","ok","slackOAuthErrorReason","error","is_enterprise_install","team","expires_in","undefined","refresh_token","access_token","accessToken","bot_user_id","botUserId","app_id","appId","scope","id","name","teamId","teamName","scopes","split","map","value","trim","filter","Boolean","operation","request","status","statusText","response","warn","retryAfterSeconds","errName","Error","key","Object","entries","serialized","String","JSON","stringify","set","path","URL","SLACK_API_BASE_URL","toString","retryAfter","get","parsed","Number","parseInt","isNaN"],"mappings":"AAAA,SAAQA,MAAM,QAAO,8BAA8B;AACnD,OAAOC,MAAKC,SAAS,EAAEC,YAAY,QAAO,KAAK;AAC/C,SAAQC,MAAM,QAAO,aAAa;AAClC,SACEC,sCAAsC,EACtCC,6BAA6B,EAC7BC,kCAAkC,QAC7B,kBAAkB;AAEzB,MAAMC,uBAAuB;AAwC7B,OAAO,SAASC;IACd,OAAO;QACL,MAAMC,2BAA0BC,KAAK;YACnC,MAAMC,OAAO,MAAMC,cAAc,+BAA+B,IAC9DZ,GACGa,IAAI,CAACC,YAAY,oBAAoB;oBACpCH,MAAM,IAAII,gBAAgB;wBACxBC,WAAWb,OAAOc,qBAAqB;wBACvCC,eAAef,OAAOgB,yBAAyB;wBAC/CC,MAAMV,MAAMU,IAAI;wBAChBC,cAAclB,OAAOmB,wBAAwB;oBAC/C;oBACAC,SAAShB;gBACX,GACCiB,IAAI;YAET,OAAOC,iBAAiBd;QAC1B;QAEA,MAAMe,aAAYhB,KAAK;YACrB,MAAME,cAAc,gBAAgB;gBAClC,MAAMZ,GAAGa,IAAI,CAACC,YAAY,gBAAgB;oBACxCa,SAAS;wBAACC,eAAe,CAAC,OAAO,EAAElB,MAAMmB,KAAK,EAAE;oBAAA;oBAChDN,SAAShB;gBACX;YACF;QACF;QAEA,MAAMuB,YAAWpB,KAAK;YACpB,OAAO,MAAMqB,oBAAoBrB,MAAMsB,MAAM,EAAE,IAC7ChC,GACGa,IAAI,CAACC,YAAYJ,MAAMsB,MAAM,GAAG;oBAC/BL,SAAS;wBAACC,eAAe,CAAC,OAAO,EAAElB,MAAMmB,KAAK,EAAE;oBAAA;oBAChDlB,MAAMsB,qBAAqBvB,MAAMwB,SAAS;oBAC1CX,SAAShB;gBACX,GACCiB,IAAI;QAEX;IACF;AACF;AAEA,SAASC,iBAAiBd,IAA8B;IACtD,IAAIA,KAAKwB,EAAE,KAAK,MAAM;QACpB,MAAM,IAAI9B,8BACR+B,sBAAsBzB,KAAK0B,KAAK,GAChC;IAEJ;IACA,IAAI1B,KAAK2B,qBAAqB,KAAK,QAAQ3B,KAAK4B,IAAI,IAAI,MAAM;QAC5D,MAAM,IAAInC;IACZ;IACA,mGAAmG;IACnG,wFAAwF;IACxF,gFAAgF;IAChF,IAAIO,KAAK6B,UAAU,KAAKC,aAAa9B,KAAK+B,aAAa,KAAKD,WAAW;QACrE,MAAM,IAAInC;IACZ;IACA,MAAM,EAACqC,cAAcC,WAAW,EAAEC,aAAaC,SAAS,EAAEC,QAAQC,KAAK,EAAET,IAAI,EAAEU,KAAK,EAAC,GAAGtC;IACxF,IACE,OAAOiC,gBAAgB,YACvB,OAAOE,cAAc,YACrB,OAAOE,UAAU,YACjB,OAAOT,KAAKW,EAAE,KAAK,YACnB,OAAOX,KAAKY,IAAI,KAAK,YACrB,OAAOF,UAAU,UACjB;QACA,MAAM,IAAI5C,8BACR,+BACA;IAEJ;IACA,OAAO;QACLuC;QACAE;QACAE;QACAI,QAAQb,KAAKW,EAAE;QACfG,UAAUd,KAAKY,IAAI;QACnBG,QAAQL,MACLM,KAAK,CAAC,KACNC,GAAG,CAAC,CAACC,QAAUA,MAAMC,IAAI,IACzBC,MAAM,CAACC;IACZ;AACF;AAEA,SAASxB,sBAAsBC,KAAc;IAC3C,IAAIA,UAAU,yBAAyBA,UAAU,kBAAkB,OAAO;IAC1E,IAAIA,UAAU,eAAe,OAAO;IACpC,OAAO;AACT;AAEA,eAAezB,cAAiBiD,SAAiB,EAAEC,OAAyB;IAC1E,IAAI;QACF,OAAO,MAAMA;IACf,EAAE,OAAOzB,OAAO;QACd,IAAIA,iBAAiBhC,+BAA+B,MAAMgC;QAC1D,IAAIA,iBAAiBpC,WAAW;YAC9B,MAAM,EAAC8D,MAAM,EAAEC,UAAU,EAAErC,OAAO,EAAC,GAAGU,MAAM4B,QAAQ;YACpDlE,SAASmE,IAAI,CAAC;gBAACL;gBAAWE;gBAAQC;YAAU,GAAG;YAC/C,IAAID,WAAW,KAAK;gBAClB,MAAM,IAAI1D,8BACR,gBACA,kCACA8D,kBAAkBxC;YAEtB;YACA,IAAIoC,UAAU,KAAK;gBACjB,MAAM,IAAI1D,8BAA8B,wBAAwB;YAClE;YACA,MAAM,IAAIA,8BAA8B,iBAAiB;QAC3D;QACA,IAAIgC,iBAAiBnC,cAAc;YACjCH,SAASmE,IAAI,CAAC;gBAACL;YAAS,GAAG;YAC3B,MAAM,IAAIxD,8BAA8B,WAAW;QACrD;QACAN,SAASmE,IAAI,CACX;YAACL;YAAWO,SAAS/B,iBAAiBgC,QAAQhC,MAAMc,IAAI,GAAG,OAAOd;QAAK,GACvE;QAEF,MAAM,IAAIhC,8BAA8B,wBAAwB;IAClE;AACF;AAEA,eAAe0B,oBAAuBC,MAAc,EAAE8B,OAAyB;IAC7E,IAAI;QACF,OAAO,MAAMA;IACf,EAAE,OAAOzB,OAAO;QACd,IAAIA,iBAAiBhC,+BAA+B,MAAMgC;QAC1D,IAAIA,iBAAiBpC,WAAW;YAC9B,MAAM,EAAC8D,MAAM,EAAEC,UAAU,EAAErC,OAAO,EAAC,GAAGU,MAAM4B,QAAQ;YACpDlE,SAASmE,IAAI,CACX;gBAACL,WAAW;gBAAe7B;gBAAQ+B;gBAAQC;YAAU,GACrD;YAEF,IAAID,WAAW,KAAK;gBAClB,MAAM,IAAI1D,8BACR,gBACA,kCACA8D,kBAAkBxC;YAEtB;YACA,IAAIoC,WAAW,KAAK;gBAClB,MAAM,IAAI1D,8BACR,qBACA;YAEJ;YACA,IAAI0D,UAAU,KAAK;gBACjB,MAAM,IAAI1D,8BAA8B,wBAAwB;YAClE;YACA,MAAM,IAAIA,8BACR,+BACA;QAEJ;QACA,IAAIgC,iBAAiBnC,cAAc;YACjCH,SAASmE,IAAI,CAAC;gBAACL,WAAW;gBAAe7B;YAAM,GAAG;YAClD,MAAM,IAAI3B,8BAA8B,WAAW;QACrD;QACAN,SAASmE,IAAI,CACX;YACEL,WAAW;YACX7B;YACAoC,SAAS/B,iBAAiBgC,QAAQhC,MAAMc,IAAI,GAAG,OAAOd;QACxD,GACA;QAEF,MAAM,IAAIhC,8BAA8B,wBAAwB;IAClE;AACF;AAEA,SAAS4B,qBAAqBvB,KAA8B;IAC1D,MAAMC,OAAO,IAAII;IACjB,KAAK,MAAM,CAACuD,KAAKb,MAAM,IAAIc,OAAOC,OAAO,CAAC9D,OAAQ;QAChD,IAAI+C,UAAU,QAAQA,UAAUhB,WAAW;QAC3C,MAAMgC,aACJ,OAAOhB,UAAU,YAAY,OAAOA,UAAU,YAAY,OAAOA,UAAU,YACvEiB,OAAOjB,SACPkB,KAAKC,SAAS,CAACnB;QACrB,IAAIgB,eAAehC,WAAW9B,KAAKkE,GAAG,CAACP,KAAKG;IAC9C;IACA,OAAO9D;AACT;AAEA,SAASG,YAAYgE,IAAY;IAC/B,OAAO,IAAIC,IAAID,MAAM,GAAG3E,OAAO6E,kBAAkB,CAAC,CAAC,CAAC,EAAEC,QAAQ;AAChE;AAEA,SAASd,kBAAkBxC,OAAgB;IACzC,MAAMuD,aAAavD,QAAQwD,GAAG,CAAC;IAC/B,IAAI,CAACD,YAAY,OAAOzC;IACxB,MAAM2C,SAASC,OAAOC,QAAQ,CAACJ,YAAY;IAC3C,OAAOG,OAAOE,KAAK,CAACH,UAAU3C,YAAY2C;AAC5C"}
|
|
1
|
+
{"version":3,"sources":["../../src/api/client.ts"],"sourcesContent":["import {logger} from '@shipfox/node-opentelemetry';\nimport ky, {HTTPError, TimeoutError} from 'ky';\nimport {config} from '#config.js';\nimport {\n SlackEnterpriseInstallUnsupportedError,\n SlackIntegrationProviderError,\n SlackTokenRotationUnsupportedError,\n} from '#core/errors.js';\n\nconst SLACK_API_TIMEOUT_MS = 10_000;\nconst trailingSlashesPattern = /\\/+$/;\n\nexport interface SlackAuthorization {\n accessToken: string;\n botUserId: string;\n appId: string;\n teamId: string;\n teamName: string;\n scopes: string[];\n}\n\nexport interface SlackWebApiResponse {\n ok: boolean;\n error?: string | undefined;\n [key: string]: unknown;\n}\n\nexport interface SlackApiClient {\n exchangeAuthorizationCode(input: {code: string}): Promise<SlackAuthorization>;\n revokeToken(input: {token: string}): Promise<void>;\n callMethod(input: {\n method: string;\n token: string;\n arguments: Record<string, unknown>;\n }): Promise<SlackWebApiResponse>;\n}\n\ninterface SlackOAuthAccessResponse {\n ok?: unknown;\n error?: unknown;\n access_token?: unknown;\n bot_user_id?: unknown;\n app_id?: unknown;\n team?: {id?: unknown; name?: unknown} | null | undefined;\n scope?: unknown;\n is_enterprise_install?: unknown;\n expires_in?: unknown;\n refresh_token?: unknown;\n}\n\nexport function createSlackApiClient(): SlackApiClient {\n return {\n async exchangeAuthorizationCode(input) {\n const body = await mapSlackError('exchange-authorization-code', () =>\n ky\n .post(slackApiUrl('oauth.v2.access'), {\n body: new URLSearchParams({\n client_id: config.SLACK_OAUTH_CLIENT_ID,\n client_secret: config.SLACK_OAUTH_CLIENT_SECRET,\n code: input.code,\n redirect_uri: config.SLACK_OAUTH_REDIRECT_URL,\n }),\n timeout: SLACK_API_TIMEOUT_MS,\n })\n .json<SlackOAuthAccessResponse>(),\n );\n return parseOAuthAccess(body);\n },\n\n async revokeToken(input) {\n await mapSlackError('revoke-token', async () => {\n await ky.post(slackApiUrl('auth.revoke'), {\n headers: {authorization: `Bearer ${input.token}`},\n timeout: SLACK_API_TIMEOUT_MS,\n });\n });\n },\n\n async callMethod(input) {\n return await mapSlackWebApiError(input.method, () =>\n ky\n .post(slackApiUrl(input.method), {\n headers: {authorization: `Bearer ${input.token}`},\n body: slackMethodArguments(input.arguments),\n timeout: SLACK_API_TIMEOUT_MS,\n })\n .json<SlackWebApiResponse>(),\n );\n },\n };\n}\n\nfunction parseOAuthAccess(body: SlackOAuthAccessResponse): SlackAuthorization {\n if (body.ok !== true) {\n throw new SlackIntegrationProviderError(\n slackOAuthErrorReason(body.error),\n 'Slack authorization request failed',\n );\n }\n if (body.is_enterprise_install === true || body.team == null) {\n throw new SlackEnterpriseInstallUnsupportedError();\n }\n // The bot-token store has no refresh path, so a rotating (expiring) token must never be persisted.\n // Slack omits both fields when rotation is off, so presence of either signals rotation.\n // The rejected token self-expires, matching the Enterprise Grid rejection path.\n if (body.expires_in !== undefined || body.refresh_token !== undefined) {\n throw new SlackTokenRotationUnsupportedError();\n }\n const {access_token: accessToken, bot_user_id: botUserId, app_id: appId, team, scope} = body;\n if (\n typeof accessToken !== 'string' ||\n typeof botUserId !== 'string' ||\n typeof appId !== 'string' ||\n typeof team.id !== 'string' ||\n typeof team.name !== 'string' ||\n typeof scope !== 'string'\n ) {\n throw new SlackIntegrationProviderError(\n 'malformed-provider-response',\n 'Slack authorization response did not include the required installation details',\n );\n }\n return {\n accessToken,\n botUserId,\n appId,\n teamId: team.id,\n teamName: team.name,\n scopes: scope\n .split(',')\n .map((value) => value.trim())\n .filter(Boolean),\n };\n}\n\nfunction slackOAuthErrorReason(error: unknown) {\n if (error === 'service_unavailable' || error === 'internal_error') return 'provider-unavailable';\n if (error === 'ratelimited') return 'rate-limited';\n return 'access-denied';\n}\n\nasync function mapSlackError<T>(operation: string, request: () => Promise<T>): Promise<T> {\n try {\n return await request();\n } catch (error) {\n if (error instanceof SlackIntegrationProviderError) throw error;\n if (error instanceof HTTPError) {\n const {status, statusText, headers} = error.response;\n logger().warn({operation, status, statusText}, 'Slack API request rejected');\n if (status === 429) {\n throw new SlackIntegrationProviderError(\n 'rate-limited',\n 'Slack request was rate limited',\n retryAfterSeconds(headers),\n );\n }\n if (status >= 500) {\n throw new SlackIntegrationProviderError('provider-unavailable', 'Slack request failed');\n }\n throw new SlackIntegrationProviderError('access-denied', 'Slack request was rejected');\n }\n if (error instanceof TimeoutError) {\n logger().warn({operation}, 'Slack API request timed out');\n throw new SlackIntegrationProviderError('timeout', 'Slack request timed out');\n }\n logger().warn(\n {operation, errName: error instanceof Error ? error.name : typeof error},\n 'Slack API request failed',\n );\n throw new SlackIntegrationProviderError('provider-unavailable', 'Slack request failed');\n }\n}\n\nasync function mapSlackWebApiError<T>(method: string, request: () => Promise<T>): Promise<T> {\n try {\n return await request();\n } catch (error) {\n if (error instanceof SlackIntegrationProviderError) throw error;\n if (error instanceof HTTPError) {\n const {status, statusText, headers} = error.response;\n logger().warn(\n {operation: 'call-method', method, status, statusText},\n 'Slack API request rejected',\n );\n if (status === 429) {\n throw new SlackIntegrationProviderError(\n 'rate-limited',\n 'Slack request was rate limited',\n retryAfterSeconds(headers),\n );\n }\n if (status === 413) {\n throw new SlackIntegrationProviderError(\n 'content-too-large',\n 'Slack request content was too large',\n );\n }\n if (status >= 500) {\n throw new SlackIntegrationProviderError('provider-unavailable', 'Slack request failed');\n }\n throw new SlackIntegrationProviderError(\n 'malformed-provider-response',\n 'Slack request was rejected',\n );\n }\n if (error instanceof TimeoutError) {\n logger().warn({operation: 'call-method', method}, 'Slack API request timed out');\n throw new SlackIntegrationProviderError('timeout', 'Slack request timed out');\n }\n logger().warn(\n {\n operation: 'call-method',\n method,\n errName: error instanceof Error ? error.name : typeof error,\n },\n 'Slack API request failed',\n );\n throw new SlackIntegrationProviderError('provider-unavailable', 'Slack request failed');\n }\n}\n\nfunction slackMethodArguments(input: Record<string, unknown>): URLSearchParams {\n const body = new URLSearchParams();\n for (const [key, value] of Object.entries(input)) {\n if (value === null || value === undefined) continue;\n const serialized =\n typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'\n ? String(value)\n : JSON.stringify(value);\n if (serialized !== undefined) body.set(key, serialized);\n }\n return body;\n}\n\nfunction slackApiUrl(path: string): string {\n const baseUrl = config.SLACK_API_BASE_URL.replace(trailingSlashesPattern, '');\n return new URL(path, `${baseUrl}/`).toString();\n}\n\nfunction retryAfterSeconds(headers: Headers): number | undefined {\n const retryAfter = headers.get('retry-after');\n if (!retryAfter) return undefined;\n const parsed = Number.parseInt(retryAfter, 10);\n return Number.isNaN(parsed) ? undefined : parsed;\n}\n"],"names":["logger","ky","HTTPError","TimeoutError","config","SlackEnterpriseInstallUnsupportedError","SlackIntegrationProviderError","SlackTokenRotationUnsupportedError","SLACK_API_TIMEOUT_MS","trailingSlashesPattern","createSlackApiClient","exchangeAuthorizationCode","input","body","mapSlackError","post","slackApiUrl","URLSearchParams","client_id","SLACK_OAUTH_CLIENT_ID","client_secret","SLACK_OAUTH_CLIENT_SECRET","code","redirect_uri","SLACK_OAUTH_REDIRECT_URL","timeout","json","parseOAuthAccess","revokeToken","headers","authorization","token","callMethod","mapSlackWebApiError","method","slackMethodArguments","arguments","ok","slackOAuthErrorReason","error","is_enterprise_install","team","expires_in","undefined","refresh_token","access_token","accessToken","bot_user_id","botUserId","app_id","appId","scope","id","name","teamId","teamName","scopes","split","map","value","trim","filter","Boolean","operation","request","status","statusText","response","warn","retryAfterSeconds","errName","Error","key","Object","entries","serialized","String","JSON","stringify","set","path","baseUrl","SLACK_API_BASE_URL","replace","URL","toString","retryAfter","get","parsed","Number","parseInt","isNaN"],"mappings":"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"}
|
package/dist/core/webhook.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { GetIntegrationConnectionByIdFn, IntegrationTx, PublishIntegrationEventReceivedFn, RecordDeliveryOnlyFn } from '@shipfox/api-integration-core-dto';
|
|
1
|
+
import type { ClaimWebhookDeliveryFn, GetIntegrationConnectionByIdFn, IntegrationTx, PublishIntegrationEventReceivedFn, RecordDeliveryOnlyFn } from '@shipfox/api-integration-core-dto';
|
|
2
2
|
import { type SlackEventBaseEnvelopeDto, type SlackSlashCommandDto } from '@shipfox/api-integration-slack-dto';
|
|
3
|
-
export type SlackWebhookOutcome = 'published' | 'duplicate' | 'unknown-team' | 'revoked-installation' | 'missing-connection' | 'inactive-connection' | 'unsupported-event' | 'self-message';
|
|
3
|
+
export type SlackWebhookOutcome = 'published' | 'duplicate' | 'unknown-team' | 'revoked-installation' | 'revoked' | 'unaffected-revocation' | 'stale-lifecycle-event' | 'missing-connection' | 'inactive-connection' | 'unsupported-event' | 'self-message';
|
|
4
4
|
interface SlackWebhookParams {
|
|
5
5
|
tx: IntegrationTx;
|
|
6
6
|
deliveryId: string;
|
|
@@ -10,15 +10,17 @@ interface SlackWebhookParams {
|
|
|
10
10
|
}
|
|
11
11
|
export interface HandleSlackEventParams extends SlackWebhookParams {
|
|
12
12
|
envelope: SlackEventBaseEnvelopeDto;
|
|
13
|
+
claimWebhookDelivery: ClaimWebhookDeliveryFn;
|
|
13
14
|
}
|
|
14
15
|
export interface HandleSlackCommandParams extends SlackWebhookParams {
|
|
15
16
|
command: SlackSlashCommandDto;
|
|
16
17
|
}
|
|
18
|
+
type SlackCommandOutcome = Exclude<SlackWebhookOutcome, 'unsupported-event' | 'self-message' | 'revoked' | 'unaffected-revocation' | 'stale-lifecycle-event'>;
|
|
17
19
|
export declare function handleSlackEvent(params: HandleSlackEventParams): Promise<{
|
|
18
20
|
outcome: SlackWebhookOutcome;
|
|
19
21
|
}>;
|
|
20
22
|
export declare function handleSlackCommand(params: HandleSlackCommandParams): Promise<{
|
|
21
|
-
outcome:
|
|
23
|
+
outcome: SlackCommandOutcome;
|
|
22
24
|
}>;
|
|
23
25
|
export declare function isSelfAuthoredSlackEvent(event: unknown, botUserId: string): boolean;
|
|
24
26
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/core/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,8BAA8B,EAE9B,aAAa,EACb,iCAAiC,EACjC,oBAAoB,EACrB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/core/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,8BAA8B,EAE9B,aAAa,EACb,iCAAiC,EACjC,oBAAoB,EACrB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAIL,KAAK,yBAAyB,EAE9B,KAAK,oBAAoB,EAI1B,MAAM,oCAAoC,CAAC;AAuB5C,MAAM,MAAM,mBAAmB,GAC3B,WAAW,GACX,WAAW,GACX,cAAc,GACd,sBAAsB,GACtB,SAAS,GACT,uBAAuB,GACvB,uBAAuB,GACvB,oBAAoB,GACpB,qBAAqB,GACrB,mBAAmB,GACnB,cAAc,CAAC;AAEnB,UAAU,kBAAkB;IAC1B,EAAE,EAAE,aAAa,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,+BAA+B,EAAE,iCAAiC,CAAC;IACnE,kBAAkB,EAAE,oBAAoB,CAAC;IACzC,4BAA4B,EAAE,8BAA8B,CAAC;CAC9D;AAED,MAAM,WAAW,sBAAuB,SAAQ,kBAAkB;IAChE,QAAQ,EAAE,yBAAyB,CAAC;IACpC,oBAAoB,EAAE,sBAAsB,CAAC;CAC9C;AAED,MAAM,WAAW,wBAAyB,SAAQ,kBAAkB;IAClE,OAAO,EAAE,oBAAoB,CAAC;CAC/B;AASD,KAAK,mBAAmB,GAAG,OAAO,CAChC,mBAAmB,EACjB,mBAAmB,GACnB,cAAc,GACd,SAAS,GACT,uBAAuB,GACvB,uBAAuB,CAC1B,CAAC;AAGF,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC;IAAC,OAAO,EAAE,mBAAmB,CAAA;CAAC,CAAC,CAmDzC;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC;IAAC,OAAO,EAAE,mBAAmB,CAAA;CAAC,CAAC,CAwBzC;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAUnF"}
|
package/dist/core/webhook.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { SLACK_PROVIDER, SLACK_SLASH_COMMAND_EVENT, slackEventEnvelopeSchema } from '@shipfox/api-integration-slack-dto';
|
|
1
|
+
import { SLACK_APP_UNINSTALLED_EVENT, SLACK_PROVIDER, SLACK_SLASH_COMMAND_EVENT, slackEventEnvelopeSchema, slackLifecycleEventTypes, slackTokensRevokedEventSchema } from '@shipfox/api-integration-slack-dto';
|
|
2
2
|
import { logger } from '@shipfox/node-opentelemetry';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { getSlackInstallationByTeamId } from '#db/installations.js';
|
|
4
|
+
import { getSlackInstallationByTeamId, markSlackInstallationRevoked } from '#db/installations.js';
|
|
5
5
|
const slackSelfAuthoredEventSchema = z.object({
|
|
6
6
|
bot_id: z.string().optional(),
|
|
7
7
|
user: z.string().optional(),
|
|
@@ -11,6 +11,8 @@ const slackSelfAuthoredEventSchema = z.object({
|
|
|
11
11
|
}).passthrough().optional()
|
|
12
12
|
}).passthrough();
|
|
13
13
|
export async function handleSlackEvent(params) {
|
|
14
|
+
const lifecycleType = asSlackLifecycleEventType(params.envelope.event.type);
|
|
15
|
+
if (lifecycleType) return handleSlackLifecycleEvent(params, lifecycleType);
|
|
14
16
|
const resolution = await resolveSlackConnection({
|
|
15
17
|
...params,
|
|
16
18
|
teamId: params.envelope.team_id
|
|
@@ -89,6 +91,103 @@ export function isSelfAuthoredSlackEvent(event, botUserId) {
|
|
|
89
91
|
const nestedMessage = parsed.data.message;
|
|
90
92
|
return parsed.data.bot_id !== undefined || parsed.data.user === botUserId || nestedMessage?.bot_id !== undefined || nestedMessage?.user === botUserId;
|
|
91
93
|
}
|
|
94
|
+
function asSlackLifecycleEventType(eventType) {
|
|
95
|
+
return slackLifecycleEventTypes.find((type)=>type === eventType);
|
|
96
|
+
}
|
|
97
|
+
async function handleSlackLifecycleEvent(params, lifecycleType) {
|
|
98
|
+
const claim = await params.claimWebhookDelivery({
|
|
99
|
+
tx: params.tx,
|
|
100
|
+
provider: SLACK_PROVIDER,
|
|
101
|
+
deliveryId: params.deliveryId
|
|
102
|
+
});
|
|
103
|
+
if (!claim.claimed) {
|
|
104
|
+
logger().info({
|
|
105
|
+
deliveryId: params.deliveryId,
|
|
106
|
+
teamId: params.envelope.team_id,
|
|
107
|
+
lifecycleType
|
|
108
|
+
}, 'slack lifecycle event: duplicate delivery, dropping');
|
|
109
|
+
return {
|
|
110
|
+
outcome: 'duplicate'
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const installation = await getSlackInstallationByTeamId(params.envelope.team_id, {
|
|
114
|
+
tx: params.tx
|
|
115
|
+
});
|
|
116
|
+
if (!installation) {
|
|
117
|
+
logger().warn({
|
|
118
|
+
deliveryId: params.deliveryId,
|
|
119
|
+
teamId: params.envelope.team_id,
|
|
120
|
+
lifecycleType
|
|
121
|
+
}, 'slack lifecycle event: unknown team, dropping');
|
|
122
|
+
return {
|
|
123
|
+
outcome: 'unknown-team'
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (installation.status !== 'installed') {
|
|
127
|
+
logger().info({
|
|
128
|
+
deliveryId: params.deliveryId,
|
|
129
|
+
teamId: params.envelope.team_id,
|
|
130
|
+
connectionId: installation.connectionId,
|
|
131
|
+
lifecycleType
|
|
132
|
+
}, 'slack lifecycle event: installation is not installed, dropping');
|
|
133
|
+
return {
|
|
134
|
+
outcome: 'revoked-installation'
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (isSlackLifecycleEventOlderThanInstallation(params.envelope.event_time, installation.updatedAt)) {
|
|
138
|
+
logger().info({
|
|
139
|
+
deliveryId: params.deliveryId,
|
|
140
|
+
teamId: params.envelope.team_id,
|
|
141
|
+
connectionId: installation.connectionId
|
|
142
|
+
}, 'slack lifecycle event: predates the current installation, dropping');
|
|
143
|
+
return {
|
|
144
|
+
outcome: 'stale-lifecycle-event'
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const revokesInstallation = lifecycleType === SLACK_APP_UNINSTALLED_EVENT || slackTokensRevokedAffectsBot(params.envelope.event, installation.botUserId);
|
|
148
|
+
if (!revokesInstallation) {
|
|
149
|
+
logger().info({
|
|
150
|
+
deliveryId: params.deliveryId,
|
|
151
|
+
teamId: params.envelope.team_id,
|
|
152
|
+
connectionId: installation.connectionId
|
|
153
|
+
}, 'slack lifecycle event: token revocation does not affect installation bot, dropping');
|
|
154
|
+
return {
|
|
155
|
+
outcome: 'unaffected-revocation'
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const revoked = await markSlackInstallationRevoked(installation.connectionId, {
|
|
159
|
+
tx: params.tx,
|
|
160
|
+
expectedGeneration: installation.generation
|
|
161
|
+
});
|
|
162
|
+
if (!revoked) {
|
|
163
|
+
logger().info({
|
|
164
|
+
deliveryId: params.deliveryId,
|
|
165
|
+
teamId: params.envelope.team_id,
|
|
166
|
+
connectionId: installation.connectionId
|
|
167
|
+
}, 'slack lifecycle event: installation changed before revocation, dropping');
|
|
168
|
+
return {
|
|
169
|
+
outcome: 'stale-lifecycle-event'
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
logger().info({
|
|
173
|
+
deliveryId: params.deliveryId,
|
|
174
|
+
teamId: params.envelope.team_id,
|
|
175
|
+
connectionId: installation.connectionId,
|
|
176
|
+
lifecycleType
|
|
177
|
+
}, 'slack lifecycle event: installation revoked');
|
|
178
|
+
return {
|
|
179
|
+
outcome: 'revoked'
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function slackTokensRevokedAffectsBot(event, botUserId) {
|
|
183
|
+
const parsed = slackTokensRevokedEventSchema.safeParse(event);
|
|
184
|
+
// Only a named bot token proves this installation credential was revoked; OAuth-only and missing lists do not.
|
|
185
|
+
return parsed.success && (parsed.data.tokens?.bot?.includes(botUserId) ?? false);
|
|
186
|
+
}
|
|
187
|
+
function isSlackLifecycleEventOlderThanInstallation(eventTime, installationUpdatedAt) {
|
|
188
|
+
// Slack event_time has second precision, so an event stamped in the installation second is not provably stale.
|
|
189
|
+
return eventTime < Math.floor(installationUpdatedAt.getTime() / 1000);
|
|
190
|
+
}
|
|
92
191
|
async function resolveSlackConnection(params) {
|
|
93
192
|
const installation = await getSlackInstallationByTeamId(params.teamId, {
|
|
94
193
|
tx: params.tx
|
package/dist/core/webhook.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/webhook.ts"],"sourcesContent":["import type {\n GetIntegrationConnectionByIdFn,\n IntegrationConnection,\n IntegrationTx,\n PublishIntegrationEventReceivedFn,\n RecordDeliveryOnlyFn,\n} from '@shipfox/api-integration-core-dto';\nimport {\n SLACK_PROVIDER,\n SLACK_SLASH_COMMAND_EVENT,\n type SlackEventBaseEnvelopeDto,\n type SlackSlashCommandDto,\n slackEventEnvelopeSchema,\n} from '@shipfox/api-integration-slack-dto';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {z} from 'zod';\nimport {getSlackInstallationByTeamId, type SlackInstallation} from '#db/installations.js';\n\nconst slackSelfAuthoredEventSchema = z\n .object({\n bot_id: z.string().optional(),\n user: z.string().optional(),\n message: z\n .object({\n bot_id: z.string().optional(),\n user: z.string().optional(),\n })\n .passthrough()\n .optional(),\n })\n .passthrough();\n\nexport type SlackWebhookOutcome =\n | 'published'\n | 'duplicate'\n | 'unknown-team'\n | 'revoked-installation'\n | 'missing-connection'\n | 'inactive-connection'\n | 'unsupported-event'\n | 'self-message';\n\ninterface SlackWebhookParams {\n tx: IntegrationTx;\n deliveryId: string;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n}\n\nexport interface HandleSlackEventParams extends SlackWebhookParams {\n envelope: SlackEventBaseEnvelopeDto;\n}\n\nexport interface HandleSlackCommandParams extends SlackWebhookParams {\n command: SlackSlashCommandDto;\n}\n\ntype SlackConnectionResolution =\n | {kind: 'ok'; connection: IntegrationConnection; installation: SlackInstallation}\n | {\n kind: 'drop';\n outcome: Exclude<\n SlackWebhookOutcome,\n 'published' | 'duplicate' | 'unsupported-event' | 'self-message'\n >;\n };\n\nexport async function handleSlackEvent(\n params: HandleSlackEventParams,\n): Promise<{outcome: SlackWebhookOutcome}> {\n const resolution = await resolveSlackConnection({\n ...params,\n teamId: params.envelope.team_id,\n });\n if (resolution.kind === 'drop') return resolution;\n\n if (isSelfAuthoredSlackEvent(params.envelope.event, resolution.installation.botUserId)) {\n await recordSlackDeliveryOnly(params);\n return {outcome: 'self-message'};\n }\n\n const supported = slackEventEnvelopeSchema.safeParse(params.envelope);\n if (!supported.success) {\n logger().info(\n {\n deliveryId: params.deliveryId,\n teamId: params.envelope.team_id,\n type: params.envelope.event.type,\n },\n 'slack webhook: unsupported event, dropping',\n );\n await recordSlackDeliveryOnly(params);\n return {outcome: 'unsupported-event'};\n }\n\n const result = await params.publishIntegrationEventReceived({\n tx: params.tx,\n event: {\n provider: SLACK_PROVIDER,\n source: resolution.connection.slug,\n event: supported.data.event.type,\n workspaceId: resolution.connection.workspaceId,\n connectionId: resolution.connection.id,\n connectionName: resolution.connection.displayName,\n deliveryId: params.deliveryId,\n receivedAt: new Date().toISOString(),\n payload: {\n ...supported.data.event,\n team_id: supported.data.team_id,\n api_app_id: supported.data.api_app_id,\n event_id: supported.data.event_id,\n event_time: supported.data.event_time,\n },\n },\n });\n\n return {outcome: result.published ? 'published' : 'duplicate'};\n}\n\nexport async function handleSlackCommand(\n params: HandleSlackCommandParams,\n): Promise<{outcome: Exclude<SlackWebhookOutcome, 'unsupported-event' | 'self-message'>}> {\n const resolution = await resolveSlackConnection({\n ...params,\n teamId: params.command.team_id,\n });\n if (resolution.kind === 'drop') return resolution;\n\n const {token: _token, ...payload} = params.command;\n const result = await params.publishIntegrationEventReceived({\n tx: params.tx,\n event: {\n provider: SLACK_PROVIDER,\n source: resolution.connection.slug,\n event: SLACK_SLASH_COMMAND_EVENT,\n workspaceId: resolution.connection.workspaceId,\n connectionId: resolution.connection.id,\n connectionName: resolution.connection.displayName,\n deliveryId: params.deliveryId,\n receivedAt: new Date().toISOString(),\n payload,\n },\n });\n\n return {outcome: result.published ? 'published' : 'duplicate'};\n}\n\nexport function isSelfAuthoredSlackEvent(event: unknown, botUserId: string): boolean {\n const parsed = slackSelfAuthoredEventSchema.safeParse(event);\n if (!parsed.success) return false;\n const nestedMessage = parsed.data.message;\n return (\n parsed.data.bot_id !== undefined ||\n parsed.data.user === botUserId ||\n nestedMessage?.bot_id !== undefined ||\n nestedMessage?.user === botUserId\n );\n}\n\nasync function resolveSlackConnection(\n params: SlackWebhookParams & {teamId: string},\n): Promise<SlackConnectionResolution> {\n const installation = await getSlackInstallationByTeamId(params.teamId, {tx: params.tx});\n if (!installation) {\n logger().warn(\n {deliveryId: params.deliveryId, teamId: params.teamId},\n 'slack webhook: unknown team, dropping',\n );\n await recordSlackDeliveryOnly(params);\n return {kind: 'drop', outcome: 'unknown-team'};\n }\n\n if (installation.status !== 'installed') {\n logger().info(\n {\n deliveryId: params.deliveryId,\n teamId: params.teamId,\n connectionId: installation.connectionId,\n },\n 'slack webhook: installation is not installed, dropping',\n );\n await recordSlackDeliveryOnly(params);\n return {kind: 'drop', outcome: 'revoked-installation'};\n }\n\n const connection = await params.getIntegrationConnectionById(installation.connectionId, {\n tx: params.tx,\n });\n if (!connection) {\n logger().warn(\n {\n deliveryId: params.deliveryId,\n teamId: params.teamId,\n connectionId: installation.connectionId,\n },\n 'slack webhook: installation has no connection, dropping',\n );\n await recordSlackDeliveryOnly(params);\n return {kind: 'drop', outcome: 'missing-connection'};\n }\n\n if (connection.lifecycleStatus !== 'active') {\n logger().info(\n {deliveryId: params.deliveryId, teamId: params.teamId, connectionId: connection.id},\n 'slack webhook: inactive connection, dropping',\n );\n await recordSlackDeliveryOnly(params);\n return {kind: 'drop', outcome: 'inactive-connection'};\n }\n\n return {kind: 'ok', connection, installation};\n}\n\nasync function recordSlackDeliveryOnly(\n params: Pick<SlackWebhookParams, 'tx' | 'deliveryId' | 'recordDeliveryOnly'>,\n): Promise<void> {\n await params.recordDeliveryOnly({\n tx: params.tx,\n provider: SLACK_PROVIDER,\n deliveryId: params.deliveryId,\n });\n}\n"],"names":["SLACK_PROVIDER","SLACK_SLASH_COMMAND_EVENT","slackEventEnvelopeSchema","logger","z","getSlackInstallationByTeamId","slackSelfAuthoredEventSchema","object","bot_id","string","optional","user","message","passthrough","handleSlackEvent","params","resolution","resolveSlackConnection","teamId","envelope","team_id","kind","isSelfAuthoredSlackEvent","event","installation","botUserId","recordSlackDeliveryOnly","outcome","supported","safeParse","success","info","deliveryId","type","result","publishIntegrationEventReceived","tx","provider","source","connection","slug","data","workspaceId","connectionId","id","connectionName","displayName","receivedAt","Date","toISOString","payload","api_app_id","event_id","event_time","published","handleSlackCommand","command","token","_token","parsed","nestedMessage","undefined","warn","status","getIntegrationConnectionById","lifecycleStatus","recordDeliveryOnly"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/core/webhook.ts"],"sourcesContent":["import type {\n ClaimWebhookDeliveryFn,\n GetIntegrationConnectionByIdFn,\n IntegrationConnection,\n IntegrationTx,\n PublishIntegrationEventReceivedFn,\n RecordDeliveryOnlyFn,\n} from '@shipfox/api-integration-core-dto';\nimport {\n SLACK_APP_UNINSTALLED_EVENT,\n SLACK_PROVIDER,\n SLACK_SLASH_COMMAND_EVENT,\n type SlackEventBaseEnvelopeDto,\n type SlackLifecycleEventType,\n type SlackSlashCommandDto,\n slackEventEnvelopeSchema,\n slackLifecycleEventTypes,\n slackTokensRevokedEventSchema,\n} from '@shipfox/api-integration-slack-dto';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {z} from 'zod';\nimport {\n getSlackInstallationByTeamId,\n markSlackInstallationRevoked,\n type SlackInstallation,\n} from '#db/installations.js';\n\nconst slackSelfAuthoredEventSchema = z\n .object({\n bot_id: z.string().optional(),\n user: z.string().optional(),\n message: z\n .object({\n bot_id: z.string().optional(),\n user: z.string().optional(),\n })\n .passthrough()\n .optional(),\n })\n .passthrough();\n\nexport type SlackWebhookOutcome =\n | 'published'\n | 'duplicate'\n | 'unknown-team'\n | 'revoked-installation'\n | 'revoked'\n | 'unaffected-revocation'\n | 'stale-lifecycle-event'\n | 'missing-connection'\n | 'inactive-connection'\n | 'unsupported-event'\n | 'self-message';\n\ninterface SlackWebhookParams {\n tx: IntegrationTx;\n deliveryId: string;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n}\n\nexport interface HandleSlackEventParams extends SlackWebhookParams {\n envelope: SlackEventBaseEnvelopeDto;\n claimWebhookDelivery: ClaimWebhookDeliveryFn;\n}\n\nexport interface HandleSlackCommandParams extends SlackWebhookParams {\n command: SlackSlashCommandDto;\n}\n\ntype SlackConnectionResolution =\n | {kind: 'ok'; connection: IntegrationConnection; installation: SlackInstallation}\n | {\n kind: 'drop';\n outcome: SlackConnectionDropOutcome;\n };\n\ntype SlackCommandOutcome = Exclude<\n SlackWebhookOutcome,\n | 'unsupported-event'\n | 'self-message'\n | 'revoked'\n | 'unaffected-revocation'\n | 'stale-lifecycle-event'\n>;\ntype SlackConnectionDropOutcome = Exclude<SlackCommandOutcome, 'published' | 'duplicate'>;\n\nexport async function handleSlackEvent(\n params: HandleSlackEventParams,\n): Promise<{outcome: SlackWebhookOutcome}> {\n const lifecycleType = asSlackLifecycleEventType(params.envelope.event.type);\n if (lifecycleType) return handleSlackLifecycleEvent(params, lifecycleType);\n\n const resolution = await resolveSlackConnection({\n ...params,\n teamId: params.envelope.team_id,\n });\n if (resolution.kind === 'drop') return resolution;\n\n if (isSelfAuthoredSlackEvent(params.envelope.event, resolution.installation.botUserId)) {\n await recordSlackDeliveryOnly(params);\n return {outcome: 'self-message'};\n }\n\n const supported = slackEventEnvelopeSchema.safeParse(params.envelope);\n if (!supported.success) {\n logger().info(\n {\n deliveryId: params.deliveryId,\n teamId: params.envelope.team_id,\n type: params.envelope.event.type,\n },\n 'slack webhook: unsupported event, dropping',\n );\n await recordSlackDeliveryOnly(params);\n return {outcome: 'unsupported-event'};\n }\n\n const result = await params.publishIntegrationEventReceived({\n tx: params.tx,\n event: {\n provider: SLACK_PROVIDER,\n source: resolution.connection.slug,\n event: supported.data.event.type,\n workspaceId: resolution.connection.workspaceId,\n connectionId: resolution.connection.id,\n connectionName: resolution.connection.displayName,\n deliveryId: params.deliveryId,\n receivedAt: new Date().toISOString(),\n payload: {\n ...supported.data.event,\n team_id: supported.data.team_id,\n api_app_id: supported.data.api_app_id,\n event_id: supported.data.event_id,\n event_time: supported.data.event_time,\n },\n },\n });\n\n return {outcome: result.published ? 'published' : 'duplicate'};\n}\n\nexport async function handleSlackCommand(\n params: HandleSlackCommandParams,\n): Promise<{outcome: SlackCommandOutcome}> {\n const resolution = await resolveSlackConnection({\n ...params,\n teamId: params.command.team_id,\n });\n if (resolution.kind === 'drop') return resolution;\n\n const {token: _token, ...payload} = params.command;\n const result = await params.publishIntegrationEventReceived({\n tx: params.tx,\n event: {\n provider: SLACK_PROVIDER,\n source: resolution.connection.slug,\n event: SLACK_SLASH_COMMAND_EVENT,\n workspaceId: resolution.connection.workspaceId,\n connectionId: resolution.connection.id,\n connectionName: resolution.connection.displayName,\n deliveryId: params.deliveryId,\n receivedAt: new Date().toISOString(),\n payload,\n },\n });\n\n return {outcome: result.published ? 'published' : 'duplicate'};\n}\n\nexport function isSelfAuthoredSlackEvent(event: unknown, botUserId: string): boolean {\n const parsed = slackSelfAuthoredEventSchema.safeParse(event);\n if (!parsed.success) return false;\n const nestedMessage = parsed.data.message;\n return (\n parsed.data.bot_id !== undefined ||\n parsed.data.user === botUserId ||\n nestedMessage?.bot_id !== undefined ||\n nestedMessage?.user === botUserId\n );\n}\n\nfunction asSlackLifecycleEventType(eventType: string): SlackLifecycleEventType | undefined {\n return slackLifecycleEventTypes.find((type) => type === eventType);\n}\n\nasync function handleSlackLifecycleEvent(\n params: HandleSlackEventParams,\n lifecycleType: SlackLifecycleEventType,\n): Promise<{outcome: SlackWebhookOutcome}> {\n const claim = await params.claimWebhookDelivery({\n tx: params.tx,\n provider: SLACK_PROVIDER,\n deliveryId: params.deliveryId,\n });\n if (!claim.claimed) {\n logger().info(\n {deliveryId: params.deliveryId, teamId: params.envelope.team_id, lifecycleType},\n 'slack lifecycle event: duplicate delivery, dropping',\n );\n return {outcome: 'duplicate'};\n }\n\n const installation = await getSlackInstallationByTeamId(params.envelope.team_id, {tx: params.tx});\n if (!installation) {\n logger().warn(\n {deliveryId: params.deliveryId, teamId: params.envelope.team_id, lifecycleType},\n 'slack lifecycle event: unknown team, dropping',\n );\n return {outcome: 'unknown-team'};\n }\n\n if (installation.status !== 'installed') {\n logger().info(\n {\n deliveryId: params.deliveryId,\n teamId: params.envelope.team_id,\n connectionId: installation.connectionId,\n lifecycleType,\n },\n 'slack lifecycle event: installation is not installed, dropping',\n );\n return {outcome: 'revoked-installation'};\n }\n\n if (\n isSlackLifecycleEventOlderThanInstallation(params.envelope.event_time, installation.updatedAt)\n ) {\n logger().info(\n {\n deliveryId: params.deliveryId,\n teamId: params.envelope.team_id,\n connectionId: installation.connectionId,\n },\n 'slack lifecycle event: predates the current installation, dropping',\n );\n return {outcome: 'stale-lifecycle-event'};\n }\n\n const revokesInstallation =\n lifecycleType === SLACK_APP_UNINSTALLED_EVENT ||\n slackTokensRevokedAffectsBot(params.envelope.event, installation.botUserId);\n if (!revokesInstallation) {\n logger().info(\n {\n deliveryId: params.deliveryId,\n teamId: params.envelope.team_id,\n connectionId: installation.connectionId,\n },\n 'slack lifecycle event: token revocation does not affect installation bot, dropping',\n );\n return {outcome: 'unaffected-revocation'};\n }\n\n const revoked = await markSlackInstallationRevoked(installation.connectionId, {\n tx: params.tx,\n expectedGeneration: installation.generation,\n });\n if (!revoked) {\n logger().info(\n {\n deliveryId: params.deliveryId,\n teamId: params.envelope.team_id,\n connectionId: installation.connectionId,\n },\n 'slack lifecycle event: installation changed before revocation, dropping',\n );\n return {outcome: 'stale-lifecycle-event'};\n }\n\n logger().info(\n {\n deliveryId: params.deliveryId,\n teamId: params.envelope.team_id,\n connectionId: installation.connectionId,\n lifecycleType,\n },\n 'slack lifecycle event: installation revoked',\n );\n return {outcome: 'revoked'};\n}\n\nfunction slackTokensRevokedAffectsBot(event: unknown, botUserId: string): boolean {\n const parsed = slackTokensRevokedEventSchema.safeParse(event);\n // Only a named bot token proves this installation credential was revoked; OAuth-only and missing lists do not.\n return parsed.success && (parsed.data.tokens?.bot?.includes(botUserId) ?? false);\n}\n\nfunction isSlackLifecycleEventOlderThanInstallation(\n eventTime: number,\n installationUpdatedAt: Date,\n): boolean {\n // Slack event_time has second precision, so an event stamped in the installation second is not provably stale.\n return eventTime < Math.floor(installationUpdatedAt.getTime() / 1000);\n}\n\nasync function resolveSlackConnection(\n params: SlackWebhookParams & {teamId: string},\n): Promise<SlackConnectionResolution> {\n const installation = await getSlackInstallationByTeamId(params.teamId, {tx: params.tx});\n if (!installation) {\n logger().warn(\n {deliveryId: params.deliveryId, teamId: params.teamId},\n 'slack webhook: unknown team, dropping',\n );\n await recordSlackDeliveryOnly(params);\n return {kind: 'drop', outcome: 'unknown-team'};\n }\n\n if (installation.status !== 'installed') {\n logger().info(\n {\n deliveryId: params.deliveryId,\n teamId: params.teamId,\n connectionId: installation.connectionId,\n },\n 'slack webhook: installation is not installed, dropping',\n );\n await recordSlackDeliveryOnly(params);\n return {kind: 'drop', outcome: 'revoked-installation'};\n }\n\n const connection = await params.getIntegrationConnectionById(installation.connectionId, {\n tx: params.tx,\n });\n if (!connection) {\n logger().warn(\n {\n deliveryId: params.deliveryId,\n teamId: params.teamId,\n connectionId: installation.connectionId,\n },\n 'slack webhook: installation has no connection, dropping',\n );\n await recordSlackDeliveryOnly(params);\n return {kind: 'drop', outcome: 'missing-connection'};\n }\n\n if (connection.lifecycleStatus !== 'active') {\n logger().info(\n {deliveryId: params.deliveryId, teamId: params.teamId, connectionId: connection.id},\n 'slack webhook: inactive connection, dropping',\n );\n await recordSlackDeliveryOnly(params);\n return {kind: 'drop', outcome: 'inactive-connection'};\n }\n\n return {kind: 'ok', connection, installation};\n}\n\nasync function recordSlackDeliveryOnly(\n params: Pick<SlackWebhookParams, 'tx' | 'deliveryId' | 'recordDeliveryOnly'>,\n): Promise<void> {\n await params.recordDeliveryOnly({\n tx: params.tx,\n provider: SLACK_PROVIDER,\n deliveryId: params.deliveryId,\n });\n}\n"],"names":["SLACK_APP_UNINSTALLED_EVENT","SLACK_PROVIDER","SLACK_SLASH_COMMAND_EVENT","slackEventEnvelopeSchema","slackLifecycleEventTypes","slackTokensRevokedEventSchema","logger","z","getSlackInstallationByTeamId","markSlackInstallationRevoked","slackSelfAuthoredEventSchema","object","bot_id","string","optional","user","message","passthrough","handleSlackEvent","params","lifecycleType","asSlackLifecycleEventType","envelope","event","type","handleSlackLifecycleEvent","resolution","resolveSlackConnection","teamId","team_id","kind","isSelfAuthoredSlackEvent","installation","botUserId","recordSlackDeliveryOnly","outcome","supported","safeParse","success","info","deliveryId","result","publishIntegrationEventReceived","tx","provider","source","connection","slug","data","workspaceId","connectionId","id","connectionName","displayName","receivedAt","Date","toISOString","payload","api_app_id","event_id","event_time","published","handleSlackCommand","command","token","_token","parsed","nestedMessage","undefined","eventType","find","claim","claimWebhookDelivery","claimed","warn","status","isSlackLifecycleEventOlderThanInstallation","updatedAt","revokesInstallation","slackTokensRevokedAffectsBot","revoked","expectedGeneration","generation","tokens","bot","includes","eventTime","installationUpdatedAt","Math","floor","getTime","getIntegrationConnectionById","lifecycleStatus","recordDeliveryOnly"],"mappings":"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"}
|
package/dist/db/db.d.ts
CHANGED
|
@@ -144,6 +144,23 @@ export declare const schema: {
|
|
|
144
144
|
}, {}, {
|
|
145
145
|
$type: import("./installations.js").SlackInstallationStatus;
|
|
146
146
|
}>;
|
|
147
|
+
generation: import("drizzle-orm/pg-core").PgColumn<{
|
|
148
|
+
name: "generation";
|
|
149
|
+
tableName: "installations";
|
|
150
|
+
dataType: "number";
|
|
151
|
+
columnType: "PgInteger";
|
|
152
|
+
data: number;
|
|
153
|
+
driverParam: string | number;
|
|
154
|
+
notNull: true;
|
|
155
|
+
hasDefault: true;
|
|
156
|
+
isPrimaryKey: false;
|
|
157
|
+
isAutoincrement: false;
|
|
158
|
+
hasRuntimeDefault: false;
|
|
159
|
+
enumValues: undefined;
|
|
160
|
+
baseColumn: never;
|
|
161
|
+
identity: undefined;
|
|
162
|
+
generated: undefined;
|
|
163
|
+
}, {}, {}>;
|
|
147
164
|
tokenExpiresAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
148
165
|
name: "token_expires_at";
|
|
149
166
|
tableName: "installations";
|
|
@@ -344,6 +361,23 @@ export declare function db(): NodePgDatabase<{
|
|
|
344
361
|
}, {}, {
|
|
345
362
|
$type: import("./installations.js").SlackInstallationStatus;
|
|
346
363
|
}>;
|
|
364
|
+
generation: import("drizzle-orm/pg-core").PgColumn<{
|
|
365
|
+
name: "generation";
|
|
366
|
+
tableName: "installations";
|
|
367
|
+
dataType: "number";
|
|
368
|
+
columnType: "PgInteger";
|
|
369
|
+
data: number;
|
|
370
|
+
driverParam: string | number;
|
|
371
|
+
notNull: true;
|
|
372
|
+
hasDefault: true;
|
|
373
|
+
isPrimaryKey: false;
|
|
374
|
+
isAutoincrement: false;
|
|
375
|
+
hasRuntimeDefault: false;
|
|
376
|
+
enumValues: undefined;
|
|
377
|
+
baseColumn: never;
|
|
378
|
+
identity: undefined;
|
|
379
|
+
generated: undefined;
|
|
380
|
+
}, {}, {}>;
|
|
347
381
|
tokenExpiresAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
348
382
|
name: "token_expires_at";
|
|
349
383
|
tableName: "installations";
|
package/dist/db/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/db/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAInE,eAAO,MAAM,MAAM
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/db/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAInE,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAuB,CAAC;AAI3C,wBAAgB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAGjB;AAED,wBAAgB,OAAO,IAAI,IAAI,CAE9B"}
|
|
@@ -8,6 +8,7 @@ export interface SlackInstallation {
|
|
|
8
8
|
botUserId: string;
|
|
9
9
|
scopes: string[];
|
|
10
10
|
status: SlackInstallationStatus;
|
|
11
|
+
generation: number;
|
|
11
12
|
tokenExpiresAt: Date | null;
|
|
12
13
|
createdAt: Date;
|
|
13
14
|
updatedAt: Date;
|
|
@@ -36,5 +37,6 @@ export declare function deleteSlackInstallationByConnectionId(connectionId: stri
|
|
|
36
37
|
}): Promise<boolean>;
|
|
37
38
|
export declare function markSlackInstallationRevoked(connectionId: string, options?: {
|
|
38
39
|
tx?: unknown;
|
|
40
|
+
expectedGeneration?: number | undefined;
|
|
39
41
|
}): Promise<SlackInstallation | undefined>;
|
|
40
42
|
//# sourceMappingURL=installations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"installations.d.ts","sourceRoot":"","sources":["../../src/db/installations.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,uBAAuB,GAAG,WAAW,GAAG,SAAS,CAAC;AAE9D,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,uBAAuB,CAAC;IAChC,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,6BAA6B;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,uBAAuB,CAAC;IAChC,cAAc,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC;CAC1C;AAKD,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,6BAA6B,EACrC,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,iBAAiB,CAAC,
|
|
1
|
+
{"version":3,"file":"installations.d.ts","sourceRoot":"","sources":["../../src/db/installations.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,uBAAuB,GAAG,WAAW,GAAG,SAAS,CAAC;AAE9D,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,uBAAuB,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,6BAA6B;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,uBAAuB,CAAC;IAChC,cAAc,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC;CAC1C;AAKD,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,6BAA6B,EACrC,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,iBAAiB,CAAC,CA4C5B;AAED,wBAAsB,4BAA4B,CAChD,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAUxC;AAED,wBAAsB,kCAAkC,CACtD,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAUxC;AAED,wBAAsB,qCAAqC,CACzD,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,OAAO,CAAC,CAMlB;AAED,wBAAsB,4BAA4B,CAChD,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAM,GACpE,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAexC"}
|
package/dist/db/installations.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isUniqueViolation } from '@shipfox/node-drizzle';
|
|
2
|
-
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { and, eq, sql } from 'drizzle-orm';
|
|
3
3
|
import { SlackConnectionAlreadyLinkedError, SlackInstallationAlreadyLinkedError } from '#core/errors.js';
|
|
4
4
|
import { db } from './db.js';
|
|
5
5
|
import { slackInstallations, toSlackInstallation } from './schema/installations.js';
|
|
@@ -28,6 +28,7 @@ export async function upsertSlackInstallation(params, options = {}) {
|
|
|
28
28
|
botUserId: params.botUserId,
|
|
29
29
|
scopes: params.scopes,
|
|
30
30
|
status: params.status,
|
|
31
|
+
generation: sql`${slackInstallations.generation} + 1`,
|
|
31
32
|
tokenExpiresAt: params.tokenExpiresAt ?? null,
|
|
32
33
|
updatedAt: now
|
|
33
34
|
}
|
|
@@ -62,10 +63,11 @@ export async function deleteSlackInstallationByConnectionId(connectionId, option
|
|
|
62
63
|
}
|
|
63
64
|
export async function markSlackInstallationRevoked(connectionId, options = {}) {
|
|
64
65
|
const executor = options.tx ?? db();
|
|
66
|
+
const expectedInstallation = options.expectedGeneration ? and(eq(slackInstallations.connectionId, connectionId), eq(slackInstallations.generation, options.expectedGeneration)) : eq(slackInstallations.connectionId, connectionId);
|
|
65
67
|
const [row] = await executor.update(slackInstallations).set({
|
|
66
68
|
status: 'revoked',
|
|
67
69
|
updatedAt: new Date()
|
|
68
|
-
}).where(
|
|
70
|
+
}).where(expectedInstallation).returning();
|
|
69
71
|
if (!row) return undefined;
|
|
70
72
|
return toSlackInstallation(row);
|
|
71
73
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/db/installations.ts"],"sourcesContent":["import {isUniqueViolation} from '@shipfox/node-drizzle';\nimport {eq} from 'drizzle-orm';\nimport {\n SlackConnectionAlreadyLinkedError,\n SlackInstallationAlreadyLinkedError,\n} from '#core/errors.js';\nimport {db} from './db.js';\nimport {slackInstallations, toSlackInstallation} from './schema/installations.js';\n\nexport type SlackInstallationStatus = 'installed' | 'revoked';\n\nexport interface SlackInstallation {\n id: string;\n connectionId: string;\n teamId: string;\n teamName: string;\n appId: string;\n botUserId: string;\n scopes: string[];\n status: SlackInstallationStatus;\n tokenExpiresAt: Date | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface UpsertSlackInstallationParams {\n connectionId: string;\n teamId: string;\n teamName: string;\n appId: string;\n botUserId: string;\n scopes: string[];\n status: SlackInstallationStatus;\n tokenExpiresAt?: Date | null | undefined;\n}\n\ntype SlackDb = ReturnType<typeof db>;\ntype SlackTx = Parameters<Parameters<SlackDb['transaction']>[0]>[0];\n\nexport async function upsertSlackInstallation(\n params: UpsertSlackInstallationParams,\n options: {tx?: unknown} = {},\n): Promise<SlackInstallation> {\n const executor = (options.tx ?? db()) as SlackDb | SlackTx;\n const now = new Date();\n let row: typeof slackInstallations.$inferSelect | undefined;\n\n try {\n [row] = await executor\n .insert(slackInstallations)\n .values({\n connectionId: params.connectionId,\n teamId: params.teamId,\n teamName: params.teamName,\n appId: params.appId,\n botUserId: params.botUserId,\n scopes: params.scopes,\n status: params.status,\n tokenExpiresAt: params.tokenExpiresAt ?? null,\n })\n .onConflictDoUpdate({\n target: slackInstallations.teamId,\n setWhere: eq(slackInstallations.connectionId, params.connectionId),\n set: {\n connectionId: params.connectionId,\n teamId: params.teamId,\n teamName: params.teamName,\n appId: params.appId,\n botUserId: params.botUserId,\n scopes: params.scopes,\n status: params.status,\n tokenExpiresAt: params.tokenExpiresAt ?? null,\n updatedAt: now,\n },\n })\n .returning();\n } catch (error) {\n if (isUniqueViolation(error, 'integrations_slack_installations_connection_unique')) {\n throw new SlackConnectionAlreadyLinkedError(params.connectionId);\n }\n throw error;\n }\n\n if (!row) throw new SlackInstallationAlreadyLinkedError(params.teamId);\n return toSlackInstallation(row);\n}\n\nexport async function getSlackInstallationByTeamId(\n teamId: string,\n options: {tx?: unknown} = {},\n): Promise<SlackInstallation | undefined> {\n const executor = (options.tx ?? db()) as SlackDb | SlackTx;\n const rows = await executor\n .select()\n .from(slackInstallations)\n .where(eq(slackInstallations.teamId, teamId))\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toSlackInstallation(row);\n}\n\nexport async function getSlackInstallationByConnectionId(\n connectionId: string,\n options: {tx?: unknown} = {},\n): Promise<SlackInstallation | undefined> {\n const executor = (options.tx ?? db()) as SlackDb | SlackTx;\n const rows = await executor\n .select()\n .from(slackInstallations)\n .where(eq(slackInstallations.connectionId, connectionId))\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toSlackInstallation(row);\n}\n\nexport async function deleteSlackInstallationByConnectionId(\n connectionId: string,\n options: {tx?: unknown} = {},\n): Promise<boolean> {\n const executor = (options.tx ?? db()) as SlackDb | SlackTx;\n const result = await executor\n .delete(slackInstallations)\n .where(eq(slackInstallations.connectionId, connectionId));\n return (result.rowCount ?? 0) > 0;\n}\n\nexport async function markSlackInstallationRevoked(\n connectionId: string,\n options: {tx?: unknown} = {},\n): Promise<SlackInstallation | undefined> {\n const executor = (options.tx ?? db()) as SlackDb | SlackTx;\n const [row] = await executor\n .update(slackInstallations)\n .set({status: 'revoked', updatedAt: new Date()})\n .where(
|
|
1
|
+
{"version":3,"sources":["../../src/db/installations.ts"],"sourcesContent":["import {isUniqueViolation} from '@shipfox/node-drizzle';\nimport {and, eq, sql} from 'drizzle-orm';\nimport {\n SlackConnectionAlreadyLinkedError,\n SlackInstallationAlreadyLinkedError,\n} from '#core/errors.js';\nimport {db} from './db.js';\nimport {slackInstallations, toSlackInstallation} from './schema/installations.js';\n\nexport type SlackInstallationStatus = 'installed' | 'revoked';\n\nexport interface SlackInstallation {\n id: string;\n connectionId: string;\n teamId: string;\n teamName: string;\n appId: string;\n botUserId: string;\n scopes: string[];\n status: SlackInstallationStatus;\n generation: number;\n tokenExpiresAt: Date | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface UpsertSlackInstallationParams {\n connectionId: string;\n teamId: string;\n teamName: string;\n appId: string;\n botUserId: string;\n scopes: string[];\n status: SlackInstallationStatus;\n tokenExpiresAt?: Date | null | undefined;\n}\n\ntype SlackDb = ReturnType<typeof db>;\ntype SlackTx = Parameters<Parameters<SlackDb['transaction']>[0]>[0];\n\nexport async function upsertSlackInstallation(\n params: UpsertSlackInstallationParams,\n options: {tx?: unknown} = {},\n): Promise<SlackInstallation> {\n const executor = (options.tx ?? db()) as SlackDb | SlackTx;\n const now = new Date();\n let row: typeof slackInstallations.$inferSelect | undefined;\n\n try {\n [row] = await executor\n .insert(slackInstallations)\n .values({\n connectionId: params.connectionId,\n teamId: params.teamId,\n teamName: params.teamName,\n appId: params.appId,\n botUserId: params.botUserId,\n scopes: params.scopes,\n status: params.status,\n tokenExpiresAt: params.tokenExpiresAt ?? null,\n })\n .onConflictDoUpdate({\n target: slackInstallations.teamId,\n setWhere: eq(slackInstallations.connectionId, params.connectionId),\n set: {\n connectionId: params.connectionId,\n teamId: params.teamId,\n teamName: params.teamName,\n appId: params.appId,\n botUserId: params.botUserId,\n scopes: params.scopes,\n status: params.status,\n generation: sql`${slackInstallations.generation} + 1`,\n tokenExpiresAt: params.tokenExpiresAt ?? null,\n updatedAt: now,\n },\n })\n .returning();\n } catch (error) {\n if (isUniqueViolation(error, 'integrations_slack_installations_connection_unique')) {\n throw new SlackConnectionAlreadyLinkedError(params.connectionId);\n }\n throw error;\n }\n\n if (!row) throw new SlackInstallationAlreadyLinkedError(params.teamId);\n return toSlackInstallation(row);\n}\n\nexport async function getSlackInstallationByTeamId(\n teamId: string,\n options: {tx?: unknown} = {},\n): Promise<SlackInstallation | undefined> {\n const executor = (options.tx ?? db()) as SlackDb | SlackTx;\n const rows = await executor\n .select()\n .from(slackInstallations)\n .where(eq(slackInstallations.teamId, teamId))\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toSlackInstallation(row);\n}\n\nexport async function getSlackInstallationByConnectionId(\n connectionId: string,\n options: {tx?: unknown} = {},\n): Promise<SlackInstallation | undefined> {\n const executor = (options.tx ?? db()) as SlackDb | SlackTx;\n const rows = await executor\n .select()\n .from(slackInstallations)\n .where(eq(slackInstallations.connectionId, connectionId))\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toSlackInstallation(row);\n}\n\nexport async function deleteSlackInstallationByConnectionId(\n connectionId: string,\n options: {tx?: unknown} = {},\n): Promise<boolean> {\n const executor = (options.tx ?? db()) as SlackDb | SlackTx;\n const result = await executor\n .delete(slackInstallations)\n .where(eq(slackInstallations.connectionId, connectionId));\n return (result.rowCount ?? 0) > 0;\n}\n\nexport async function markSlackInstallationRevoked(\n connectionId: string,\n options: {tx?: unknown; expectedGeneration?: number | undefined} = {},\n): Promise<SlackInstallation | undefined> {\n const executor = (options.tx ?? db()) as SlackDb | SlackTx;\n const expectedInstallation = options.expectedGeneration\n ? and(\n eq(slackInstallations.connectionId, connectionId),\n eq(slackInstallations.generation, options.expectedGeneration),\n )\n : eq(slackInstallations.connectionId, connectionId);\n const [row] = await executor\n .update(slackInstallations)\n .set({status: 'revoked', updatedAt: new Date()})\n .where(expectedInstallation)\n .returning();\n if (!row) return undefined;\n return toSlackInstallation(row);\n}\n"],"names":["isUniqueViolation","and","eq","sql","SlackConnectionAlreadyLinkedError","SlackInstallationAlreadyLinkedError","db","slackInstallations","toSlackInstallation","upsertSlackInstallation","params","options","executor","tx","now","Date","row","insert","values","connectionId","teamId","teamName","appId","botUserId","scopes","status","tokenExpiresAt","onConflictDoUpdate","target","setWhere","set","generation","updatedAt","returning","error","getSlackInstallationByTeamId","rows","select","from","where","limit","undefined","getSlackInstallationByConnectionId","deleteSlackInstallationByConnectionId","result","delete","rowCount","markSlackInstallationRevoked","expectedInstallation","expectedGeneration","update"],"mappings":"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"}
|
|
@@ -143,6 +143,23 @@ export declare const slackInstallations: import("drizzle-orm/pg-core").PgTableWi
|
|
|
143
143
|
}, {}, {
|
|
144
144
|
$type: import("#db/installations.js").SlackInstallationStatus;
|
|
145
145
|
}>;
|
|
146
|
+
generation: import("drizzle-orm/pg-core").PgColumn<{
|
|
147
|
+
name: "generation";
|
|
148
|
+
tableName: "installations";
|
|
149
|
+
dataType: "number";
|
|
150
|
+
columnType: "PgInteger";
|
|
151
|
+
data: number;
|
|
152
|
+
driverParam: string | number;
|
|
153
|
+
notNull: true;
|
|
154
|
+
hasDefault: true;
|
|
155
|
+
isPrimaryKey: false;
|
|
156
|
+
isAutoincrement: false;
|
|
157
|
+
hasRuntimeDefault: false;
|
|
158
|
+
enumValues: undefined;
|
|
159
|
+
baseColumn: never;
|
|
160
|
+
identity: undefined;
|
|
161
|
+
generated: undefined;
|
|
162
|
+
}, {}, {}>;
|
|
146
163
|
tokenExpiresAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
147
164
|
name: "token_expires_at";
|
|
148
165
|
tableName: "installations";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"installations.d.ts","sourceRoot":"","sources":["../../../src/db/schema/installations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,sBAAsB,CAAC;AAG5D,eAAO,MAAM,kBAAkB
|
|
1
|
+
{"version":3,"file":"installations.d.ts","sourceRoot":"","sources":["../../../src/db/schema/installations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,sBAAsB,CAAC;AAG5D,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoB9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,OAAO,kBAAkB,CAAC,YAAY,CAAC;AACzE,MAAM,MAAM,yBAAyB,GAAG,OAAO,kBAAkB,CAAC,YAAY,CAAC;AAE/E,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,mBAAmB,GAAG,iBAAiB,CAe/E"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { uuidv7PrimaryKey } from '@shipfox/node-drizzle';
|
|
2
|
-
import { jsonb, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core';
|
|
2
|
+
import { integer, jsonb, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core';
|
|
3
3
|
import { pgTable } from './common.js';
|
|
4
4
|
export const slackInstallations = pgTable('installations', {
|
|
5
5
|
id: uuidv7PrimaryKey(),
|
|
@@ -10,6 +10,7 @@ export const slackInstallations = pgTable('installations', {
|
|
|
10
10
|
botUserId: text('bot_user_id').notNull(),
|
|
11
11
|
scopes: jsonb('scopes').notNull().$type(),
|
|
12
12
|
status: text('status').notNull().$type(),
|
|
13
|
+
generation: integer('generation').notNull().default(1),
|
|
13
14
|
tokenExpiresAt: timestamp('token_expires_at', {
|
|
14
15
|
withTimezone: true
|
|
15
16
|
}),
|
|
@@ -33,6 +34,7 @@ export function toSlackInstallation(row) {
|
|
|
33
34
|
botUserId: row.botUserId,
|
|
34
35
|
scopes: row.scopes,
|
|
35
36
|
status: row.status,
|
|
37
|
+
generation: row.generation,
|
|
36
38
|
tokenExpiresAt: row.tokenExpiresAt,
|
|
37
39
|
createdAt: row.createdAt,
|
|
38
40
|
updatedAt: row.updatedAt
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/db/schema/installations.ts"],"sourcesContent":["import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';\nimport {jsonb, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';\nimport type {SlackInstallation} from '#db/installations.js';\nimport {pgTable} from './common.js';\n\nexport const slackInstallations = pgTable(\n 'installations',\n {\n id: uuidv7PrimaryKey(),\n connectionId: uuid('connection_id').notNull(),\n teamId: text('team_id').notNull(),\n teamName: text('team_name').notNull(),\n appId: text('app_id').notNull(),\n botUserId: text('bot_user_id').notNull(),\n scopes: jsonb('scopes').notNull().$type<string[]>(),\n status: text('status').notNull().$type<SlackInstallation['status']>(),\n tokenExpiresAt: timestamp('token_expires_at', {withTimezone: true}),\n createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),\n },\n (table) => [\n uniqueIndex('integrations_slack_installations_connection_unique').on(table.connectionId),\n uniqueIndex('integrations_slack_installations_team_unique').on(table.teamId),\n ],\n);\n\nexport type SlackInstallationDb = typeof slackInstallations.$inferSelect;\nexport type SlackInstallationCreateDb = typeof slackInstallations.$inferInsert;\n\nexport function toSlackInstallation(row: SlackInstallationDb): SlackInstallation {\n return {\n id: row.id,\n connectionId: row.connectionId,\n teamId: row.teamId,\n teamName: row.teamName,\n appId: row.appId,\n botUserId: row.botUserId,\n scopes: row.scopes,\n status: row.status,\n tokenExpiresAt: row.tokenExpiresAt,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n"],"names":["uuidv7PrimaryKey","jsonb","text","timestamp","uniqueIndex","uuid","pgTable","slackInstallations","id","connectionId","notNull","teamId","teamName","appId","botUserId","scopes","$type","status","tokenExpiresAt","withTimezone","createdAt","defaultNow","updatedAt","table","on","toSlackInstallation","row"],"mappings":"AAAA,SAAQA,gBAAgB,QAAO,wBAAwB;AACvD,SAAQC,KAAK,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,IAAI,QAAO,sBAAsB;
|
|
1
|
+
{"version":3,"sources":["../../../src/db/schema/installations.ts"],"sourcesContent":["import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';\nimport {integer, jsonb, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';\nimport type {SlackInstallation} from '#db/installations.js';\nimport {pgTable} from './common.js';\n\nexport const slackInstallations = pgTable(\n 'installations',\n {\n id: uuidv7PrimaryKey(),\n connectionId: uuid('connection_id').notNull(),\n teamId: text('team_id').notNull(),\n teamName: text('team_name').notNull(),\n appId: text('app_id').notNull(),\n botUserId: text('bot_user_id').notNull(),\n scopes: jsonb('scopes').notNull().$type<string[]>(),\n status: text('status').notNull().$type<SlackInstallation['status']>(),\n generation: integer('generation').notNull().default(1),\n tokenExpiresAt: timestamp('token_expires_at', {withTimezone: true}),\n createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),\n },\n (table) => [\n uniqueIndex('integrations_slack_installations_connection_unique').on(table.connectionId),\n uniqueIndex('integrations_slack_installations_team_unique').on(table.teamId),\n ],\n);\n\nexport type SlackInstallationDb = typeof slackInstallations.$inferSelect;\nexport type SlackInstallationCreateDb = typeof slackInstallations.$inferInsert;\n\nexport function toSlackInstallation(row: SlackInstallationDb): SlackInstallation {\n return {\n id: row.id,\n connectionId: row.connectionId,\n teamId: row.teamId,\n teamName: row.teamName,\n appId: row.appId,\n botUserId: row.botUserId,\n scopes: row.scopes,\n status: row.status,\n generation: row.generation,\n tokenExpiresAt: row.tokenExpiresAt,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n"],"names":["uuidv7PrimaryKey","integer","jsonb","text","timestamp","uniqueIndex","uuid","pgTable","slackInstallations","id","connectionId","notNull","teamId","teamName","appId","botUserId","scopes","$type","status","generation","default","tokenExpiresAt","withTimezone","createdAt","defaultNow","updatedAt","table","on","toSlackInstallation","row"],"mappings":"AAAA,SAAQA,gBAAgB,QAAO,wBAAwB;AACvD,SAAQC,OAAO,EAAEC,KAAK,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,IAAI,QAAO,sBAAsB;AAEvF,SAAQC,OAAO,QAAO,cAAc;AAEpC,OAAO,MAAMC,qBAAqBD,QAChC,iBACA;IACEE,IAAIT;IACJU,cAAcJ,KAAK,iBAAiBK,OAAO;IAC3CC,QAAQT,KAAK,WAAWQ,OAAO;IAC/BE,UAAUV,KAAK,aAAaQ,OAAO;IACnCG,OAAOX,KAAK,UAAUQ,OAAO;IAC7BI,WAAWZ,KAAK,eAAeQ,OAAO;IACtCK,QAAQd,MAAM,UAAUS,OAAO,GAAGM,KAAK;IACvCC,QAAQf,KAAK,UAAUQ,OAAO,GAAGM,KAAK;IACtCE,YAAYlB,QAAQ,cAAcU,OAAO,GAAGS,OAAO,CAAC;IACpDC,gBAAgBjB,UAAU,oBAAoB;QAACkB,cAAc;IAAI;IACjEC,WAAWnB,UAAU,cAAc;QAACkB,cAAc;IAAI,GAAGX,OAAO,GAAGa,UAAU;IAC7EC,WAAWrB,UAAU,cAAc;QAACkB,cAAc;IAAI,GAAGX,OAAO,GAAGa,UAAU;AAC/E,GACA,CAACE,QAAU;QACTrB,YAAY,sDAAsDsB,EAAE,CAACD,MAAMhB,YAAY;QACvFL,YAAY,gDAAgDsB,EAAE,CAACD,MAAMd,MAAM;KAC5E,EACD;AAKF,OAAO,SAASgB,oBAAoBC,GAAwB;IAC1D,OAAO;QACLpB,IAAIoB,IAAIpB,EAAE;QACVC,cAAcmB,IAAInB,YAAY;QAC9BE,QAAQiB,IAAIjB,MAAM;QAClBC,UAAUgB,IAAIhB,QAAQ;QACtBC,OAAOe,IAAIf,KAAK;QAChBC,WAAWc,IAAId,SAAS;QACxBC,QAAQa,IAAIb,MAAM;QAClBE,QAAQW,IAAIX,MAAM;QAClBC,YAAYU,IAAIV,UAAU;QAC1BE,gBAAgBQ,IAAIR,cAAc;QAClCE,WAAWM,IAAIN,SAAS;QACxBE,WAAWI,IAAIJ,SAAS;IAC1B;AACF"}
|