@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.
Files changed (46) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +24 -0
  3. package/dist/api/client.d.ts.map +1 -1
  4. package/dist/api/client.js +3 -1
  5. package/dist/api/client.js.map +1 -1
  6. package/dist/core/webhook.d.ts +5 -3
  7. package/dist/core/webhook.d.ts.map +1 -1
  8. package/dist/core/webhook.js +101 -2
  9. package/dist/core/webhook.js.map +1 -1
  10. package/dist/db/db.d.ts +34 -0
  11. package/dist/db/db.d.ts.map +1 -1
  12. package/dist/db/installations.d.ts +2 -0
  13. package/dist/db/installations.d.ts.map +1 -1
  14. package/dist/db/installations.js +4 -2
  15. package/dist/db/installations.js.map +1 -1
  16. package/dist/db/schema/installations.d.ts +17 -0
  17. package/dist/db/schema/installations.d.ts.map +1 -1
  18. package/dist/db/schema/installations.js +3 -1
  19. package/dist/db/schema/installations.js.map +1 -1
  20. package/dist/index.d.ts +21 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/presentation/routes/webhooks.d.ts +2 -1
  25. package/dist/presentation/routes/webhooks.d.ts.map +1 -1
  26. package/dist/presentation/routes/webhooks.js +1 -0
  27. package/dist/presentation/routes/webhooks.js.map +1 -1
  28. package/dist/tsconfig.test.tsbuildinfo +1 -1
  29. package/drizzle/0001_dark_tyrannus.sql +1 -0
  30. package/drizzle/meta/0001_snapshot.json +139 -0
  31. package/drizzle/meta/_journal.json +7 -0
  32. package/package.json +18 -28
  33. package/src/api/client.test.ts +24 -0
  34. package/src/api/client.ts +3 -1
  35. package/src/core/disconnect.test.ts +49 -0
  36. package/src/core/webhook.test.ts +131 -2
  37. package/src/core/webhook.ts +143 -6
  38. package/src/db/installations.test.ts +15 -0
  39. package/src/db/installations.ts +11 -3
  40. package/src/db/schema/installations.ts +3 -1
  41. package/src/index.test.ts +11 -0
  42. package/src/index.ts +11 -0
  43. package/src/presentation/routes/webhooks.test.ts +28 -1
  44. package/src/presentation/routes/webhooks.ts +3 -0
  45. package/tsconfig.build.tsbuildinfo +1 -1
  46. package/turbo.json +9 -0
@@ -1,2 +1,2 @@
1
1
  $ shipfox-swc
2
- Successfully compiled: 24 files with swc (30.2ms)
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
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAWA,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"}
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"}
@@ -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
- return new URL(path, `${config.SLACK_API_BASE_URL}/`).toString();
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');
@@ -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":"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"}
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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"}
@@ -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: Exclude<SlackWebhookOutcome, 'unsupported-event' | 'self-message'>;
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,EAGL,KAAK,yBAAyB,EAC9B,KAAK,oBAAoB,EAE1B,MAAM,oCAAoC,CAAC;AAmB5C,MAAM,MAAM,mBAAmB,GAC3B,WAAW,GACX,WAAW,GACX,cAAc,GACd,sBAAsB,GACtB,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;CACrC;AAED,MAAM,WAAW,wBAAyB,SAAQ,kBAAkB;IAClE,OAAO,EAAE,oBAAoB,CAAC;CAC/B;AAYD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC;IAAC,OAAO,EAAE,mBAAmB,CAAA;CAAC,CAAC,CAgDzC;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC,mBAAmB,EAAE,mBAAmB,GAAG,cAAc,CAAC,CAAA;CAAC,CAAC,CAwBxF;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAUnF"}
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"}
@@ -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
@@ -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":"AAOA,SACEA,cAAc,EACdC,yBAAyB,EAGzBC,wBAAwB,QACnB,qCAAqC;AAC5C,SAAQC,MAAM,QAAO,8BAA8B;AACnD,SAAQC,CAAC,QAAO,MAAM;AACtB,SAAQC,4BAA4B,QAA+B,uBAAuB;AAE1F,MAAMC,+BAA+BF,EAClCG,MAAM,CAAC;IACNC,QAAQJ,EAAEK,MAAM,GAAGC,QAAQ;IAC3BC,MAAMP,EAAEK,MAAM,GAAGC,QAAQ;IACzBE,SAASR,EACNG,MAAM,CAAC;QACNC,QAAQJ,EAAEK,MAAM,GAAGC,QAAQ;QAC3BC,MAAMP,EAAEK,MAAM,GAAGC,QAAQ;IAC3B,GACCG,WAAW,GACXH,QAAQ;AACb,GACCG,WAAW;AAsCd,OAAO,eAAeC,iBACpBC,MAA8B;IAE9B,MAAMC,aAAa,MAAMC,uBAAuB;QAC9C,GAAGF,MAAM;QACTG,QAAQH,OAAOI,QAAQ,CAACC,OAAO;IACjC;IACA,IAAIJ,WAAWK,IAAI,KAAK,QAAQ,OAAOL;IAEvC,IAAIM,yBAAyBP,OAAOI,QAAQ,CAACI,KAAK,EAAEP,WAAWQ,YAAY,CAACC,SAAS,GAAG;QACtF,MAAMC,wBAAwBX;QAC9B,OAAO;YAACY,SAAS;QAAc;IACjC;IAEA,MAAMC,YAAY1B,yBAAyB2B,SAAS,CAACd,OAAOI,QAAQ;IACpE,IAAI,CAACS,UAAUE,OAAO,EAAE;QACtB3B,SAAS4B,IAAI,CACX;YACEC,YAAYjB,OAAOiB,UAAU;YAC7Bd,QAAQH,OAAOI,QAAQ,CAACC,OAAO;YAC/Ba,MAAMlB,OAAOI,QAAQ,CAACI,KAAK,CAACU,IAAI;QAClC,GACA;QAEF,MAAMP,wBAAwBX;QAC9B,OAAO;YAACY,SAAS;QAAmB;IACtC;IAEA,MAAMO,SAAS,MAAMnB,OAAOoB,+BAA+B,CAAC;QAC1DC,IAAIrB,OAAOqB,EAAE;QACbb,OAAO;YACLc,UAAUrC;YACVsC,QAAQtB,WAAWuB,UAAU,CAACC,IAAI;YAClCjB,OAAOK,UAAUa,IAAI,CAAClB,KAAK,CAACU,IAAI;YAChCS,aAAa1B,WAAWuB,UAAU,CAACG,WAAW;YAC9CC,cAAc3B,WAAWuB,UAAU,CAACK,EAAE;YACtCC,gBAAgB7B,WAAWuB,UAAU,CAACO,WAAW;YACjDd,YAAYjB,OAAOiB,UAAU;YAC7Be,YAAY,IAAIC,OAAOC,WAAW;YAClCC,SAAS;gBACP,GAAGtB,UAAUa,IAAI,CAAClB,KAAK;gBACvBH,SAASQ,UAAUa,IAAI,CAACrB,OAAO;gBAC/B+B,YAAYvB,UAAUa,IAAI,CAACU,UAAU;gBACrCC,UAAUxB,UAAUa,IAAI,CAACW,QAAQ;gBACjCC,YAAYzB,UAAUa,IAAI,CAACY,UAAU;YACvC;QACF;IACF;IAEA,OAAO;QAAC1B,SAASO,OAAOoB,SAAS,GAAG,cAAc;IAAW;AAC/D;AAEA,OAAO,eAAeC,mBACpBxC,MAAgC;IAEhC,MAAMC,aAAa,MAAMC,uBAAuB;QAC9C,GAAGF,MAAM;QACTG,QAAQH,OAAOyC,OAAO,CAACpC,OAAO;IAChC;IACA,IAAIJ,WAAWK,IAAI,KAAK,QAAQ,OAAOL;IAEvC,MAAM,EAACyC,OAAOC,MAAM,EAAE,GAAGR,SAAQ,GAAGnC,OAAOyC,OAAO;IAClD,MAAMtB,SAAS,MAAMnB,OAAOoB,+BAA+B,CAAC;QAC1DC,IAAIrB,OAAOqB,EAAE;QACbb,OAAO;YACLc,UAAUrC;YACVsC,QAAQtB,WAAWuB,UAAU,CAACC,IAAI;YAClCjB,OAAOtB;YACPyC,aAAa1B,WAAWuB,UAAU,CAACG,WAAW;YAC9CC,cAAc3B,WAAWuB,UAAU,CAACK,EAAE;YACtCC,gBAAgB7B,WAAWuB,UAAU,CAACO,WAAW;YACjDd,YAAYjB,OAAOiB,UAAU;YAC7Be,YAAY,IAAIC,OAAOC,WAAW;YAClCC;QACF;IACF;IAEA,OAAO;QAACvB,SAASO,OAAOoB,SAAS,GAAG,cAAc;IAAW;AAC/D;AAEA,OAAO,SAAShC,yBAAyBC,KAAc,EAAEE,SAAiB;IACxE,MAAMkC,SAASrD,6BAA6BuB,SAAS,CAACN;IACtD,IAAI,CAACoC,OAAO7B,OAAO,EAAE,OAAO;IAC5B,MAAM8B,gBAAgBD,OAAOlB,IAAI,CAAC7B,OAAO;IACzC,OACE+C,OAAOlB,IAAI,CAACjC,MAAM,KAAKqD,aACvBF,OAAOlB,IAAI,CAAC9B,IAAI,KAAKc,aACrBmC,eAAepD,WAAWqD,aAC1BD,eAAejD,SAASc;AAE5B;AAEA,eAAeR,uBACbF,MAA6C;IAE7C,MAAMS,eAAe,MAAMnB,6BAA6BU,OAAOG,MAAM,EAAE;QAACkB,IAAIrB,OAAOqB,EAAE;IAAA;IACrF,IAAI,CAACZ,cAAc;QACjBrB,SAAS2D,IAAI,CACX;YAAC9B,YAAYjB,OAAOiB,UAAU;YAAEd,QAAQH,OAAOG,MAAM;QAAA,GACrD;QAEF,MAAMQ,wBAAwBX;QAC9B,OAAO;YAACM,MAAM;YAAQM,SAAS;QAAc;IAC/C;IAEA,IAAIH,aAAauC,MAAM,KAAK,aAAa;QACvC5D,SAAS4B,IAAI,CACX;YACEC,YAAYjB,OAAOiB,UAAU;YAC7Bd,QAAQH,OAAOG,MAAM;YACrByB,cAAcnB,aAAamB,YAAY;QACzC,GACA;QAEF,MAAMjB,wBAAwBX;QAC9B,OAAO;YAACM,MAAM;YAAQM,SAAS;QAAsB;IACvD;IAEA,MAAMY,aAAa,MAAMxB,OAAOiD,4BAA4B,CAACxC,aAAamB,YAAY,EAAE;QACtFP,IAAIrB,OAAOqB,EAAE;IACf;IACA,IAAI,CAACG,YAAY;QACfpC,SAAS2D,IAAI,CACX;YACE9B,YAAYjB,OAAOiB,UAAU;YAC7Bd,QAAQH,OAAOG,MAAM;YACrByB,cAAcnB,aAAamB,YAAY;QACzC,GACA;QAEF,MAAMjB,wBAAwBX;QAC9B,OAAO;YAACM,MAAM;YAAQM,SAAS;QAAoB;IACrD;IAEA,IAAIY,WAAW0B,eAAe,KAAK,UAAU;QAC3C9D,SAAS4B,IAAI,CACX;YAACC,YAAYjB,OAAOiB,UAAU;YAAEd,QAAQH,OAAOG,MAAM;YAAEyB,cAAcJ,WAAWK,EAAE;QAAA,GAClF;QAEF,MAAMlB,wBAAwBX;QAC9B,OAAO;YAACM,MAAM;YAAQM,SAAS;QAAqB;IACtD;IAEA,OAAO;QAACN,MAAM;QAAMkB;QAAYf;IAAY;AAC9C;AAEA,eAAeE,wBACbX,MAA4E;IAE5E,MAAMA,OAAOmD,kBAAkB,CAAC;QAC9B9B,IAAIrB,OAAOqB,EAAE;QACbC,UAAUrC;QACVgC,YAAYjB,OAAOiB,UAAU;IAC/B;AACF"}
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";
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAuB,CAAC;AAI3C,wBAAgB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAGjB;AAED,wBAAgB,OAAO,IAAI,IAAI,CAE9B"}
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,CA2C5B;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,CAAA;CAAM,GAC3B,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CASxC"}
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"}
@@ -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(eq(slackInstallations.connectionId, connectionId)).returning();
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(eq(slackInstallations.connectionId, connectionId))\n .returning();\n if (!row) return undefined;\n return toSlackInstallation(row);\n}\n"],"names":["isUniqueViolation","eq","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","updatedAt","returning","error","getSlackInstallationByTeamId","rows","select","from","where","limit","undefined","getSlackInstallationByConnectionId","deleteSlackInstallationByConnectionId","result","delete","rowCount","markSlackInstallationRevoked","update"],"mappings":"AAAA,SAAQA,iBAAiB,QAAO,wBAAwB;AACxD,SAAQC,EAAE,QAAO,cAAc;AAC/B,SACEC,iCAAiC,EACjCC,mCAAmC,QAC9B,kBAAkB;AACzB,SAAQC,EAAE,QAAO,UAAU;AAC3B,SAAQC,kBAAkB,EAAEC,mBAAmB,QAAO,4BAA4B;AAgClF,OAAO,eAAeC,wBACpBC,MAAqC,EACrCC,UAA0B,CAAC,CAAC;IAE5B,MAAMC,WAAYD,QAAQE,EAAE,IAAIP;IAChC,MAAMQ,MAAM,IAAIC;IAChB,IAAIC;IAEJ,IAAI;QACF,CAACA,IAAI,GAAG,MAAMJ,SACXK,MAAM,CAACV,oBACPW,MAAM,CAAC;YACNC,cAAcT,OAAOS,YAAY;YACjCC,QAAQV,OAAOU,MAAM;YACrBC,UAAUX,OAAOW,QAAQ;YACzBC,OAAOZ,OAAOY,KAAK;YACnBC,WAAWb,OAAOa,SAAS;YAC3BC,QAAQd,OAAOc,MAAM;YACrBC,QAAQf,OAAOe,MAAM;YACrBC,gBAAgBhB,OAAOgB,cAAc,IAAI;QAC3C,GACCC,kBAAkB,CAAC;YAClBC,QAAQrB,mBAAmBa,MAAM;YACjCS,UAAU1B,GAAGI,mBAAmBY,YAAY,EAAET,OAAOS,YAAY;YACjEW,KAAK;gBACHX,cAAcT,OAAOS,YAAY;gBACjCC,QAAQV,OAAOU,MAAM;gBACrBC,UAAUX,OAAOW,QAAQ;gBACzBC,OAAOZ,OAAOY,KAAK;gBACnBC,WAAWb,OAAOa,SAAS;gBAC3BC,QAAQd,OAAOc,MAAM;gBACrBC,QAAQf,OAAOe,MAAM;gBACrBC,gBAAgBhB,OAAOgB,cAAc,IAAI;gBACzCK,WAAWjB;YACb;QACF,GACCkB,SAAS;IACd,EAAE,OAAOC,OAAO;QACd,IAAI/B,kBAAkB+B,OAAO,uDAAuD;YAClF,MAAM,IAAI7B,kCAAkCM,OAAOS,YAAY;QACjE;QACA,MAAMc;IACR;IAEA,IAAI,CAACjB,KAAK,MAAM,IAAIX,oCAAoCK,OAAOU,MAAM;IACrE,OAAOZ,oBAAoBQ;AAC7B;AAEA,OAAO,eAAekB,6BACpBd,MAAc,EACdT,UAA0B,CAAC,CAAC;IAE5B,MAAMC,WAAYD,QAAQE,EAAE,IAAIP;IAChC,MAAM6B,OAAO,MAAMvB,SAChBwB,MAAM,GACNC,IAAI,CAAC9B,oBACL+B,KAAK,CAACnC,GAAGI,mBAAmBa,MAAM,EAAEA,SACpCmB,KAAK,CAAC;IACT,MAAMvB,MAAMmB,IAAI,CAAC,EAAE;IACnB,IAAI,CAACnB,KAAK,OAAOwB;IACjB,OAAOhC,oBAAoBQ;AAC7B;AAEA,OAAO,eAAeyB,mCACpBtB,YAAoB,EACpBR,UAA0B,CAAC,CAAC;IAE5B,MAAMC,WAAYD,QAAQE,EAAE,IAAIP;IAChC,MAAM6B,OAAO,MAAMvB,SAChBwB,MAAM,GACNC,IAAI,CAAC9B,oBACL+B,KAAK,CAACnC,GAAGI,mBAAmBY,YAAY,EAAEA,eAC1CoB,KAAK,CAAC;IACT,MAAMvB,MAAMmB,IAAI,CAAC,EAAE;IACnB,IAAI,CAACnB,KAAK,OAAOwB;IACjB,OAAOhC,oBAAoBQ;AAC7B;AAEA,OAAO,eAAe0B,sCACpBvB,YAAoB,EACpBR,UAA0B,CAAC,CAAC;IAE5B,MAAMC,WAAYD,QAAQE,EAAE,IAAIP;IAChC,MAAMqC,SAAS,MAAM/B,SAClBgC,MAAM,CAACrC,oBACP+B,KAAK,CAACnC,GAAGI,mBAAmBY,YAAY,EAAEA;IAC7C,OAAO,AAACwB,CAAAA,OAAOE,QAAQ,IAAI,CAAA,IAAK;AAClC;AAEA,OAAO,eAAeC,6BACpB3B,YAAoB,EACpBR,UAA0B,CAAC,CAAC;IAE5B,MAAMC,WAAYD,QAAQE,EAAE,IAAIP;IAChC,MAAM,CAACU,IAAI,GAAG,MAAMJ,SACjBmC,MAAM,CAACxC,oBACPuB,GAAG,CAAC;QAACL,QAAQ;QAAWM,WAAW,IAAIhB;IAAM,GAC7CuB,KAAK,CAACnC,GAAGI,mBAAmBY,YAAY,EAAEA,eAC1Ca,SAAS;IACZ,IAAI,CAAChB,KAAK,OAAOwB;IACjB,OAAOhC,oBAAoBQ;AAC7B"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmB9B,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,CAc/E"}
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;AAE9E,SAAQC,OAAO,QAAO,cAAc;AAEpC,OAAO,MAAMC,qBAAqBD,QAChC,iBACA;IACEE,IAAIR;IACJS,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,gBAAgBf,UAAU,oBAAoB;QAACgB,cAAc;IAAI;IACjEC,WAAWjB,UAAU,cAAc;QAACgB,cAAc;IAAI,GAAGT,OAAO,GAAGW,UAAU;IAC7EC,WAAWnB,UAAU,cAAc;QAACgB,cAAc;IAAI,GAAGT,OAAO,GAAGW,UAAU;AAC/E,GACA,CAACE,QAAU;QACTnB,YAAY,sDAAsDoB,EAAE,CAACD,MAAMd,YAAY;QACvFL,YAAY,gDAAgDoB,EAAE,CAACD,MAAMZ,MAAM;KAC5E,EACD;AAKF,OAAO,SAASc,oBAAoBC,GAAwB;IAC1D,OAAO;QACLlB,IAAIkB,IAAIlB,EAAE;QACVC,cAAciB,IAAIjB,YAAY;QAC9BE,QAAQe,IAAIf,MAAM;QAClBC,UAAUc,IAAId,QAAQ;QACtBC,OAAOa,IAAIb,KAAK;QAChBC,WAAWY,IAAIZ,SAAS;QACxBC,QAAQW,IAAIX,MAAM;QAClBE,QAAQS,IAAIT,MAAM;QAClBC,gBAAgBQ,IAAIR,cAAc;QAClCE,WAAWM,IAAIN,SAAS;QACxBE,WAAWI,IAAIJ,SAAS;IAC1B;AACF"}
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"}