mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(slack): native Sim app trigger mode via the preview-gated slack_v2 block (#5892)
* feat(slack): native Sim app trigger mode behind the preview-gated slack_v2 block Restore the native Sim Slack app mode for the slack_oauth trigger using a single-credential-picker design. The trigger is only reachable through the preview-gated slack_v2 block, so the mode inherits that gate — no separate env flag. - Re-add SIM_SUBSCRIBED_EVENTS / SLACK_SIM_EVENT_OPTIONS derived exports. - Merge the trigger credential picker (credentialKind: 'any') so it lists Sim OAuth accounts and reusable custom bots together, mirroring the slack_v2 block. - Event dropdown narrows to the Sim app's subscribed events when an OAuth account is selected, all events for a custom bot (resolved client-side from the warmed credential list). - Deploy branch discriminates by the RESOLVED credential, not a UI field: a bot credential routes by credential id (custom app); otherwise validate the event against SIM_SUBSCRIBED_EVENTS, resolve the credential owner's token, derive routingKey from Slack team_id (auth.test), and route on the shared Sim app. - The ingest endpoint and team_id fan-out routing already existed on staging. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018asmKsWQ5Vi7T7wD9uHofz * fix(slack): address review — set credentialId, scope OAuth path to workspace, drop event narrowing Review round 1 (Greptile 4/5 + Cursor Bugbot): - deploy.ts: the native Sim-app (OAuth) branch now sets providerConfig.credentialId (runtime token resolution in the slack provider and credential-disconnect cleanup both key slack_app rows on it — without it, file downloads / reaction text fail and disconnect leaves the webhook active). - deploy.ts: resolve the OAuth credential through resolveTriggerCredentialId (workspace- and oauth-scoped) so a pasted foreign/other-tenant credential id can't bind to the workflow; use the resolved canonical id for token owner lookup + routing. - deploy.ts: a deleted/secretless custom bot credential (getSlackBotCredential → null but a service_account row exists) now returns the "reconnect the bot" error instead of the misleading "connected Slack account" message. - oauth.ts: drop the credential-based event-option narrowing. The shared dropdown framework doesn't revalidate a stored value when its dependency changes, so switching a custom bot → Sim account left an orphaned event that failed deploy with a 400. The event picker now offers all events; the deploy path is the authoritative gate. - tests: add broken-bot and workspace-not-resolvable cases; assert credentialId is set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018asmKsWQ5Vi7T7wD9uHofz --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
efe1de315e
commit
5adf68f332
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { credential } from '@sim/db/schema'
|
||||
import { resetDbChainMock } from '@sim/testing'
|
||||
import { account, credential } from '@sim/db/schema'
|
||||
import { queueTableRows, resetDbChainMock } from '@sim/testing'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterAll, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
@@ -25,7 +25,33 @@ vi.mock('@/lib/webhooks/pending-verification', () => ({
|
||||
PendingWebhookVerificationTracker: vi.fn(),
|
||||
}))
|
||||
|
||||
import { buildProviderConfig, resolveTriggerCredentialId } from '@/lib/webhooks/deploy'
|
||||
const {
|
||||
mockGetSlackBotCredential,
|
||||
mockResolveOAuthAccountId,
|
||||
mockRefreshAccessTokenIfNeeded,
|
||||
mockFetchSlackTeamId,
|
||||
} = vi.hoisted(() => ({
|
||||
mockGetSlackBotCredential: vi.fn(),
|
||||
mockResolveOAuthAccountId: vi.fn(),
|
||||
mockRefreshAccessTokenIfNeeded: vi.fn(),
|
||||
mockFetchSlackTeamId: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/app/api/auth/oauth/utils', () => ({
|
||||
getSlackBotCredential: mockGetSlackBotCredential,
|
||||
resolveOAuthAccountId: mockResolveOAuthAccountId,
|
||||
refreshAccessTokenIfNeeded: mockRefreshAccessTokenIfNeeded,
|
||||
}))
|
||||
vi.mock('@/lib/webhooks/providers/slack', () => ({
|
||||
fetchSlackTeamId: mockFetchSlackTeamId,
|
||||
}))
|
||||
|
||||
import {
|
||||
buildProviderConfig,
|
||||
resolveTriggerCredentialId,
|
||||
resolveWebhookConfigForBlock,
|
||||
} from '@/lib/webhooks/deploy'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
afterAll(resetDbChainMock)
|
||||
|
||||
@@ -191,3 +217,145 @@ describe('resolveTriggerCredentialId', () => {
|
||||
expect(eq).toHaveBeenCalledWith(credential.accountId, 'credential-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveWebhookConfigForBlock — slack_oauth routing', () => {
|
||||
const slackTriggerDef = {
|
||||
provider: 'slack_app',
|
||||
name: 'Slack',
|
||||
subBlocks: [
|
||||
{ id: 'eventType', mode: 'trigger', required: true },
|
||||
{
|
||||
id: 'customBotCredential',
|
||||
mode: 'trigger',
|
||||
canonicalParamId: 'botCredential',
|
||||
serviceId: 'slack',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'manualBotCredential',
|
||||
mode: 'trigger-advanced',
|
||||
canonicalParamId: 'botCredential',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
function resolveSlack(
|
||||
values: Record<string, unknown>,
|
||||
workflow: Record<string, unknown> = { workspaceId: 'ws-1' }
|
||||
) {
|
||||
;(getBlock as unknown as Mock).mockReturnValue({ category: 'triggers' })
|
||||
;(getTrigger as unknown as Mock).mockReturnValue(slackTriggerDef)
|
||||
return resolveWebhookConfigForBlock({
|
||||
block: makeBlock('slack_oauth', values),
|
||||
workflow,
|
||||
userId: 'deployer-1',
|
||||
requestId: 'req-1',
|
||||
})
|
||||
}
|
||||
|
||||
it('routes a custom bot credential by credential id on the slack provider', async () => {
|
||||
mockGetSlackBotCredential.mockResolvedValue({ workspaceId: 'ws-1', botUserId: 'BUSER' })
|
||||
|
||||
const result = await resolveSlack({ eventType: 'message', customBotCredential: 'cred_bot_1' })
|
||||
|
||||
expect(result?.success).toBe(true)
|
||||
if (!result?.success) throw new Error('expected success')
|
||||
expect(result.config.provider).toBe('slack')
|
||||
expect(result.config.routingKey).toBe('cred_bot_1')
|
||||
expect(result.config.triggerPath).toBeNull()
|
||||
expect(result.config.providerConfig.bot_user_id).toBe('BUSER')
|
||||
})
|
||||
|
||||
it('rejects a custom bot credential from another workspace', async () => {
|
||||
mockGetSlackBotCredential.mockResolvedValue({ workspaceId: 'other-ws', botUserId: 'BUSER' })
|
||||
|
||||
const result = await resolveSlack({ eventType: 'message', customBotCredential: 'cred_bot_1' })
|
||||
|
||||
expect(result?.success).toBe(false)
|
||||
if (result?.success) throw new Error('expected failure')
|
||||
expect(result?.error?.status).toBe(400)
|
||||
expect(result?.error?.message).toContain('not available in this workspace')
|
||||
})
|
||||
|
||||
it('rejects a deleted or secretless custom bot credential as an invalid bot', async () => {
|
||||
mockGetSlackBotCredential.mockResolvedValue(null)
|
||||
mockResolveOAuthAccountId.mockResolvedValue({ credentialType: 'service_account' })
|
||||
|
||||
const result = await resolveSlack({ eventType: 'message', customBotCredential: 'cred_bot_x' })
|
||||
|
||||
expect(result?.success).toBe(false)
|
||||
if (result?.success) throw new Error('expected failure')
|
||||
expect(result?.error?.status).toBe(400)
|
||||
expect(result?.error?.message).toContain('bot credential is missing or invalid')
|
||||
expect(mockRefreshAccessTokenIfNeeded).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects an OAuth credential not resolvable in the workflow workspace', async () => {
|
||||
mockGetSlackBotCredential.mockResolvedValue(null)
|
||||
mockResolveOAuthAccountId.mockResolvedValue({ accountId: 'acct-1' })
|
||||
// No credential row queued → resolveTriggerCredentialId returns null.
|
||||
|
||||
const result = await resolveSlack({ eventType: 'message', customBotCredential: 'cred_foreign' })
|
||||
|
||||
expect(result?.success).toBe(false)
|
||||
if (result?.success) throw new Error('expected failure')
|
||||
expect(result?.error?.status).toBe(400)
|
||||
expect(result?.error?.message).toContain('not available in this workspace')
|
||||
expect(mockRefreshAccessTokenIfNeeded).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects a non-simSubscribed event on the native Sim app (OAuth account)', async () => {
|
||||
mockGetSlackBotCredential.mockResolvedValue(null)
|
||||
mockResolveOAuthAccountId.mockResolvedValue({ accountId: 'acct-1' })
|
||||
queueTableRows(credential, [{ id: 'cred_oauth_1' }])
|
||||
|
||||
const result = await resolveSlack({
|
||||
eventType: 'file_shared',
|
||||
customBotCredential: 'cred_oauth_1',
|
||||
})
|
||||
|
||||
expect(result?.success).toBe(false)
|
||||
if (result?.success) throw new Error('expected failure')
|
||||
expect(result?.error?.status).toBe(400)
|
||||
expect(result?.error?.message).toContain('not available on the Sim Slack app')
|
||||
expect(mockRefreshAccessTokenIfNeeded).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('routes an OAuth account by team_id on the slack_app provider', async () => {
|
||||
mockGetSlackBotCredential.mockResolvedValue(null)
|
||||
mockResolveOAuthAccountId.mockResolvedValue({ accountId: 'acct-1' })
|
||||
queueTableRows(credential, [{ id: 'cred_oauth_1' }])
|
||||
queueTableRows(account, [{ userId: 'owner-1' }])
|
||||
mockRefreshAccessTokenIfNeeded.mockResolvedValue('xoxb-token')
|
||||
mockFetchSlackTeamId.mockResolvedValue({ teamId: 'T123', userId: 'UBOT' })
|
||||
|
||||
const result = await resolveSlack({ eventType: 'message', customBotCredential: 'cred_oauth_1' })
|
||||
|
||||
expect(result?.success).toBe(true)
|
||||
if (!result?.success) throw new Error('expected success')
|
||||
expect(result.config.provider).toBe('slack_app')
|
||||
expect(result.config.routingKey).toBe('T123')
|
||||
expect(result.config.triggerPath).toBeNull()
|
||||
expect(result.config.providerConfig.bot_user_id).toBe('UBOT')
|
||||
// Runtime token resolution + disconnect cleanup key slack_app rows on this.
|
||||
expect(result.config.providerConfig.credentialId).toBe('cred_oauth_1')
|
||||
// Owner's token, not the deploying actor's.
|
||||
expect(mockRefreshAccessTokenIfNeeded).toHaveBeenCalledWith('cred_oauth_1', 'owner-1', 'req-1')
|
||||
})
|
||||
|
||||
it('fails when the connected Slack account token cannot be resolved', async () => {
|
||||
mockGetSlackBotCredential.mockResolvedValue(null)
|
||||
mockResolveOAuthAccountId.mockResolvedValue({ accountId: '' })
|
||||
queueTableRows(credential, [{ id: 'cred_oauth_1' }])
|
||||
mockRefreshAccessTokenIfNeeded.mockResolvedValue(null)
|
||||
|
||||
const result = await resolveSlack({ eventType: 'message', customBotCredential: 'cred_oauth_1' })
|
||||
|
||||
expect(result?.success).toBe(false)
|
||||
if (result?.success) throw new Error('expected failure')
|
||||
expect(result?.error?.status).toBe(400)
|
||||
expect(result?.error?.message).toContain('Could not access the connected Slack account')
|
||||
expect(mockFetchSlackTeamId).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
+135
-32
@@ -1,5 +1,5 @@
|
||||
import { db } from '@sim/db'
|
||||
import { credential, webhook, workflowDeploymentVersion } from '@sim/db/schema'
|
||||
import { account, credential, webhook, workflowDeploymentVersion } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { generateShortId } from '@sim/utils/id'
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
projectDesiredWebhookProviderConfig,
|
||||
} from '@/lib/webhooks/provider-subscriptions'
|
||||
import { getProviderHandler } from '@/lib/webhooks/providers'
|
||||
import { fetchSlackTeamId } from '@/lib/webhooks/providers/slack'
|
||||
import {
|
||||
prepareStableWebhookRegistrations,
|
||||
type StableDesiredWebhookRegistration,
|
||||
@@ -26,12 +27,17 @@ import {
|
||||
isCanonicalPair,
|
||||
resolveActiveCanonicalValue,
|
||||
} from '@/lib/workflows/subblocks/visibility'
|
||||
import { getSlackBotCredential } from '@/app/api/auth/oauth/utils'
|
||||
import {
|
||||
getSlackBotCredential,
|
||||
refreshAccessTokenIfNeeded,
|
||||
resolveOAuthAccountId,
|
||||
} from '@/app/api/auth/oauth/utils'
|
||||
import { getBlock } from '@/blocks'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||
import { getTrigger, isTriggerValid } from '@/triggers'
|
||||
import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants'
|
||||
import { SIM_SUBSCRIBED_EVENTS } from '@/triggers/slack/shared'
|
||||
|
||||
const logger = createLogger('DeployWebhookSync')
|
||||
|
||||
@@ -355,7 +361,12 @@ export async function resolveTriggerCredentialId(
|
||||
return resolvedCredential?.id ?? null
|
||||
}
|
||||
|
||||
async function resolveWebhookConfigForBlock(input: {
|
||||
/**
|
||||
* Resolves a trigger block to its persisted webhook config, including the
|
||||
* Slack-specific routing branch. Exported for unit testing that branch; not part
|
||||
* of the public deploy API.
|
||||
*/
|
||||
export async function resolveWebhookConfigForBlock(input: {
|
||||
block: BlockState
|
||||
workflow: Record<string, unknown>
|
||||
userId: string
|
||||
@@ -425,43 +436,135 @@ async function resolveWebhookConfigForBlock(input: {
|
||||
let effectivePath: string | null = triggerPath
|
||||
let routingKey: string | null = null
|
||||
if (triggerId === 'slack_oauth') {
|
||||
// Custom-bot only: events route by the bot credential to one shared ingest
|
||||
// URL verified with the bot's own signing secret. The native Sim-app path
|
||||
// (team_id routing via the official app) is intentionally not supported yet.
|
||||
const botCredentialId =
|
||||
// One credential picker feeds two backends. The credential's resolved kind —
|
||||
// not a UI field — picks the branch: a Slack bot credential routes by the
|
||||
// credential id (custom bring-your-own app); an OAuth account routes by Slack
|
||||
// team_id on the native shared Sim app.
|
||||
const slackCredentialId =
|
||||
typeof providerConfig.botCredential === 'string' ? providerConfig.botCredential : undefined
|
||||
if (!botCredentialId) {
|
||||
if (!slackCredentialId) {
|
||||
return {
|
||||
success: false,
|
||||
error: { message: 'Select a Slack bot credential for the trigger.', status: 400 },
|
||||
error: { message: 'Select a Slack account or bot for the trigger.', status: 400 },
|
||||
}
|
||||
}
|
||||
const botCredential = await getSlackBotCredential(botCredentialId)
|
||||
if (!botCredential) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'The selected Slack bot credential is missing or invalid. Reconnect it.',
|
||||
status: 400,
|
||||
},
|
||||
const botCredential = await getSlackBotCredential(slackCredentialId)
|
||||
if (botCredential) {
|
||||
// Custom bring-your-own bot: events route by the bot credential to one
|
||||
// shared ingest URL verified with the bot's own signing secret.
|
||||
const workflowWorkspace =
|
||||
typeof input.workflow.workspaceId === 'string' ? input.workflow.workspaceId : undefined
|
||||
if (!workflowWorkspace || botCredential.workspaceId !== workflowWorkspace) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'The selected Slack bot credential is not available in this workspace.',
|
||||
status: 400,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
const workflowWorkspace =
|
||||
typeof input.workflow.workspaceId === 'string' ? input.workflow.workspaceId : undefined
|
||||
if (!workflowWorkspace || botCredential.workspaceId !== workflowWorkspace) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'The selected Slack bot credential is not available in this workspace.',
|
||||
status: 400,
|
||||
},
|
||||
effectiveProvider = 'slack'
|
||||
effectivePath = null
|
||||
routingKey = slackCredentialId
|
||||
providerConfig.credentialId = slackCredentialId
|
||||
if (botCredential.botUserId) providerConfig.bot_user_id = botCredential.botUserId
|
||||
} else {
|
||||
// getSlackBotCredential also returns null for a custom bot credential that
|
||||
// was deleted or lost its stored secrets. Name that case so the error
|
||||
// directs the user to reconnect the bot rather than mislabeling it an OAuth
|
||||
// account below.
|
||||
const resolvedKind = await resolveOAuthAccountId(slackCredentialId)
|
||||
if (resolvedKind?.credentialType === 'service_account') {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'The selected Slack bot credential is missing or invalid. Reconnect it.',
|
||||
status: 400,
|
||||
},
|
||||
}
|
||||
}
|
||||
// Native Sim app: a workspace OAuth Slack credential. Resolve it through the
|
||||
// same workspace/provider-scoped lookup the generic credential path uses, so
|
||||
// a pasted foreign or other-tenant credential id can't bind here and the
|
||||
// canonical id is what routing and runtime token resolution key on.
|
||||
const workflowWorkspace =
|
||||
typeof input.workflow.workspaceId === 'string' ? input.workflow.workspaceId : undefined
|
||||
const resolvedCredentialId = workflowWorkspace
|
||||
? await resolveTriggerCredentialId(slackCredentialId, workflowWorkspace, 'slack')
|
||||
: null
|
||||
if (!resolvedCredentialId) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'The selected Slack credential is not available in this workspace.',
|
||||
status: 400,
|
||||
},
|
||||
}
|
||||
}
|
||||
// The shared app only subscribes to a fixed event set; reject anything
|
||||
// outside it before deriving routing.
|
||||
const eventType =
|
||||
typeof providerConfig.eventType === 'string' ? providerConfig.eventType : null
|
||||
if (!eventType || !SIM_SUBSCRIBED_EVENTS.includes(eventType)) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message:
|
||||
'This event is not available on the Sim Slack app. Use a custom bot or choose a supported event.',
|
||||
status: 400,
|
||||
},
|
||||
}
|
||||
}
|
||||
// Resolve the credential OWNER's token (not the deploying actor's) — in a
|
||||
// shared workspace a teammate can deploy a trigger wired to someone else's
|
||||
// Slack account.
|
||||
let tokenOwnerUserId = input.userId
|
||||
const resolvedAccount = await resolveOAuthAccountId(resolvedCredentialId)
|
||||
if (resolvedAccount?.accountId) {
|
||||
const [owner] = await db
|
||||
.select({ userId: account.userId })
|
||||
.from(account)
|
||||
.where(eq(account.id, resolvedAccount.accountId))
|
||||
.limit(1)
|
||||
if (owner?.userId) tokenOwnerUserId = owner.userId
|
||||
}
|
||||
const botToken = await refreshAccessTokenIfNeeded(
|
||||
resolvedCredentialId,
|
||||
tokenOwnerUserId,
|
||||
input.requestId
|
||||
)
|
||||
if (!botToken) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Could not access the connected Slack account. Reconnect it and try again.',
|
||||
status: 400,
|
||||
},
|
||||
}
|
||||
}
|
||||
try {
|
||||
const { teamId, userId: botUserId } = await fetchSlackTeamId(botToken)
|
||||
routingKey = teamId
|
||||
if (botUserId) providerConfig.bot_user_id = botUserId
|
||||
} catch (error: unknown) {
|
||||
logger.error(
|
||||
`[${input.requestId}] Slack team_id resolution failed for ${input.block.id}`,
|
||||
error
|
||||
)
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Could not verify the connected Slack workspace. Reconnect it and try again.',
|
||||
status: 400,
|
||||
},
|
||||
}
|
||||
}
|
||||
effectiveProvider = 'slack_app'
|
||||
effectivePath = null
|
||||
// Runtime token resolution and credential-disconnect cleanup key native
|
||||
// (`slack_app`) rows on providerConfig.credentialId.
|
||||
providerConfig.credentialId = resolvedCredentialId
|
||||
}
|
||||
effectiveProvider = 'slack'
|
||||
effectivePath = null
|
||||
routingKey = botCredentialId
|
||||
providerConfig.credentialId = botCredentialId
|
||||
if (botCredential.botUserId) providerConfig.bot_user_id = botCredential.botUserId
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -23,11 +23,20 @@ const BOT_FILTER_EVENTS = ['message', 'app_mention']
|
||||
const OWN_MESSAGE_EVENTS = ['message', 'app_mention', 'reaction_added', 'reaction_removed']
|
||||
|
||||
/**
|
||||
* Slack trigger backed by a reusable custom-bot credential (set up once, reused
|
||||
* across triggers and actions). Events route by that credential
|
||||
* (`webhook.routingKey = credentialId`) to one shared ingest URL, so many
|
||||
* triggers on the same bot share a single Request URL, verified with the bot's
|
||||
* own signing secret.
|
||||
* Unified Slack trigger. A single credential picker lists both the native Sim
|
||||
* Slack app (an OAuth-connected account) and reusable custom bots; the deploy
|
||||
* path resolves the credential's kind server-side to pick the backend:
|
||||
* - Custom bot: events route by that credential (`webhook.routingKey =
|
||||
* credentialId`) to one shared ingest URL verified with the bot's own signing
|
||||
* secret, so many triggers on the same bot share a single Request URL.
|
||||
* - Native Sim app: events route by Slack `team_id` on the official shared app
|
||||
* (derived at deploy time via `auth.test`, no path or app setup). Only the
|
||||
* events the shared app subscribes to are usable; the deploy path enforces
|
||||
* that (`SIM_SUBSCRIBED_EVENTS`), so the event picker offers every event
|
||||
* rather than mutating its option set with the selected credential.
|
||||
*
|
||||
* The trigger is only reachable through the preview-gated `slack_v2` block, so
|
||||
* the native Sim-app mode inherits that gate — no separate env flag.
|
||||
*/
|
||||
export const slackOAuthTrigger: TriggerConfig = {
|
||||
id: 'slack_oauth',
|
||||
@@ -51,16 +60,21 @@ export const slackOAuthTrigger: TriggerConfig = {
|
||||
},
|
||||
{
|
||||
id: 'customBotCredential',
|
||||
title: 'Slack Bot',
|
||||
title: 'Slack Account',
|
||||
type: 'oauth-input',
|
||||
canonicalParamId: 'botCredential',
|
||||
serviceId: 'slack',
|
||||
credentialKind: 'service-account',
|
||||
credentialLabels: { serviceAccountConnect: 'Set up a custom bot' },
|
||||
credentialKind: 'any',
|
||||
credentialLabels: {
|
||||
oauthGroup: 'Sim app',
|
||||
oauthConnect: 'Connect the Sim app',
|
||||
serviceAccountGroup: 'Custom bots',
|
||||
serviceAccountConnect: 'Set up a custom bot',
|
||||
},
|
||||
requiredScopes: getScopesForService('slack'),
|
||||
placeholder: 'Select a connected bot',
|
||||
placeholder: 'Select Slack account or bot',
|
||||
description:
|
||||
'Choose a custom Slack bot you set up once and reuse across triggers and actions.',
|
||||
'Connect the native Sim Slack app, or choose a custom Slack bot you set up once and reuse across triggers and actions.',
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
},
|
||||
|
||||
@@ -271,7 +271,16 @@ export const slackEventById = new Map<string, SlackEventCatalogEntry>(
|
||||
SLACK_EVENT_CATALOG.map((entry) => [entry.id, entry])
|
||||
)
|
||||
|
||||
/** Dropdown options for the event picker — every event (manifest generated on demand). */
|
||||
/**
|
||||
* Event ids the official shared Sim app subscribes to. Single source of truth
|
||||
* for the deploy-time gate that rejects a native Sim-app trigger configured with
|
||||
* an event the shared app can't deliver.
|
||||
*/
|
||||
export const SIM_SUBSCRIBED_EVENTS: readonly string[] = SLACK_EVENT_CATALOG.filter(
|
||||
(entry) => entry.simSubscribed
|
||||
).map((entry) => entry.id)
|
||||
|
||||
/** Dropdown options for the event picker — every selectable event. */
|
||||
export const SLACK_ALL_EVENT_OPTIONS = SLACK_EVENT_CATALOG.map((entry) => ({
|
||||
label: entry.label,
|
||||
id: entry.id,
|
||||
|
||||
Reference in New Issue
Block a user