Skip to content

Commit 25ea8c0

Browse files
committed
fix(catalog): preserve credential gates for automation discovery
1 parent e46504b commit 25ea8c0

10 files changed

Lines changed: 200 additions & 37 deletions

File tree

‎apps/docs/openapi-v2-resources.json‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14144,6 +14144,26 @@
1414414144
"description": "Whether the field is hidden in the editor.",
1414514145
"type": "boolean"
1414614146
},
14147+
"reactiveCondition": {
14148+
"description": "Credential-kind requirement for this field or option; inspect the selected credential before using it.",
14149+
"type": "object",
14150+
"properties": {
14151+
"watchFields": {
14152+
"type": "array",
14153+
"items": {
14154+
"type": "string"
14155+
},
14156+
"description": "Credential selector fields; the first populated value identifies the credential."
14157+
},
14158+
"requiredType": {
14159+
"type": "string",
14160+
"enum": ["oauth", "service_account"],
14161+
"description": "Credential kind required for this field or option to apply."
14162+
}
14163+
},
14164+
"required": ["watchFields", "requiredType"],
14165+
"additionalProperties": false
14166+
},
1414714167
"condition": {
1414814168
"description": "Condition under which the field applies at all.",
1414914169
"$ref": "#/components/schemas/V2CatalogCondition"
@@ -14162,6 +14182,26 @@
1416214182
"description": "Human-readable option label.",
1416314183
"type": "string"
1416414184
},
14185+
"reactiveCondition": {
14186+
"description": "Credential-kind requirement for this field or option; inspect the selected credential before using it.",
14187+
"type": "object",
14188+
"properties": {
14189+
"watchFields": {
14190+
"type": "array",
14191+
"items": {
14192+
"type": "string"
14193+
},
14194+
"description": "Credential selector fields; the first populated value identifies the credential."
14195+
},
14196+
"requiredType": {
14197+
"type": "string",
14198+
"enum": ["oauth", "service_account"],
14199+
"description": "Credential kind required for this field or option to apply."
14200+
}
14201+
},
14202+
"required": ["watchFields", "requiredType"],
14203+
"additionalProperties": false
14204+
},
1416514205
"hasIcon": {
1416614206
"description": "Whether the option renders with an icon. The icon itself is not published.",
1416714207
"type": "boolean"
@@ -14911,12 +14951,52 @@
1491114951
"label": {
1491214952
"type": "string",
1491314953
"description": "Human-readable option label."
14954+
},
14955+
"reactiveCondition": {
14956+
"description": "Credential-kind requirement for this field or option; inspect the selected credential before using it.",
14957+
"type": "object",
14958+
"properties": {
14959+
"watchFields": {
14960+
"type": "array",
14961+
"items": {
14962+
"type": "string"
14963+
},
14964+
"description": "Credential selector fields; the first populated value identifies the credential."
14965+
},
14966+
"requiredType": {
14967+
"type": "string",
14968+
"enum": ["oauth", "service_account"],
14969+
"description": "Credential kind required for this field or option to apply."
14970+
}
14971+
},
14972+
"required": ["watchFields", "requiredType"],
14973+
"additionalProperties": false
1491414974
}
1491514975
},
1491614976
"required": ["id", "label"],
1491714977
"additionalProperties": false
1491814978
}
1491914979
},
14980+
"reactiveCondition": {
14981+
"description": "Credential-kind requirement for this field or option; inspect the selected credential before using it.",
14982+
"type": "object",
14983+
"properties": {
14984+
"watchFields": {
14985+
"type": "array",
14986+
"items": {
14987+
"type": "string"
14988+
},
14989+
"description": "Credential selector fields; the first populated value identifies the credential."
14990+
},
14991+
"requiredType": {
14992+
"type": "string",
14993+
"enum": ["oauth", "service_account"],
14994+
"description": "Credential kind required for this field or option to apply."
14995+
}
14996+
},
14997+
"required": ["watchFields", "requiredType"],
14998+
"additionalProperties": false
14999+
},
1492015000
"condition": {
1492115001
"description": "Condition under which the field applies.",
1492215002
"$ref": "#/components/schemas/V2CatalogCondition"

‎apps/sim/lib/api/contracts/v2/catalog.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ const catalogDependsOnSchema = z.union([
102102
}),
103103
])
104104

105+
const catalogCredentialConditionSchema = z.object({
106+
watchFields: z
107+
.array(z.string())
108+
.describe('Credential selector fields; the first populated value identifies the credential.'),
109+
requiredType: z
110+
.enum(['oauth', 'service_account'])
111+
.describe('Credential kind required for this field or option to apply.'),
112+
})
113+
105114
/** One configuration field on a block. */
106115
export const v2BlockFieldSchema = z
107116
.object({
@@ -126,6 +135,11 @@ export const v2BlockFieldSchema = z
126135
'Where the field renders: `basic`, `advanced`, `both`, `trigger`, or `trigger-advanced`.'
127136
),
128137
hidden: z.boolean().optional().describe('Whether the field is hidden in the editor.'),
138+
reactiveCondition: catalogCredentialConditionSchema
139+
.optional()
140+
.describe(
141+
'Credential-kind requirement for this field or option; inspect the selected credential before using it.'
142+
),
129143
condition: v2CatalogConditionSchema
130144
.optional()
131145
.describe('Condition under which the field applies at all.'),
@@ -134,6 +148,11 @@ export const v2BlockFieldSchema = z
134148
z.object({
135149
id: z.string().describe('Value stored when this option is selected.'),
136150
label: z.string().optional().describe('Human-readable option label.'),
151+
reactiveCondition: catalogCredentialConditionSchema
152+
.optional()
153+
.describe(
154+
'Credential-kind requirement for this field or option; inspect the selected credential before using it.'
155+
),
137156
hasIcon: z
138157
.boolean()
139158
.optional()
@@ -557,10 +576,20 @@ const v2BlockTriggerSchema = z.object({
557576
z.object({
558577
id: z.string().describe('Value stored when this option is selected.'),
559578
label: z.string().describe('Human-readable option label.'),
579+
reactiveCondition: catalogCredentialConditionSchema
580+
.optional()
581+
.describe(
582+
'Credential-kind requirement for this field or option; inspect the selected credential before using it.'
583+
),
560584
})
561585
)
562586
.optional()
563587
.describe('Selectable options.'),
588+
reactiveCondition: catalogCredentialConditionSchema
589+
.optional()
590+
.describe(
591+
'Credential-kind requirement for this field or option; inspect the selected credential before using it.'
592+
),
564593
condition: v2CatalogConditionSchema
565594
.optional()
566595
.describe('Condition under which the field applies.'),

‎apps/sim/lib/catalog/projection/block-detail.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
resolveOperationIds,
88
} from '@/lib/catalog/projection/block-summary'
99
import {
10+
type CatalogCredentialCondition,
1011
type CatalogSubBlock,
12+
copyCredentialCondition,
1113
normalizeCondition,
1214
projectSubBlock,
1315
} from '@/lib/catalog/projection/subblock'
@@ -98,7 +100,8 @@ export interface CatalogTriggerConfigField {
98100
description?: string
99101
placeholder?: string
100102
default?: unknown
101-
options?: { id: string; label: string }[]
103+
options?: { id: string; label: string; reactiveCondition?: CatalogCredentialCondition }[]
104+
reactiveCondition?: CatalogCredentialCondition
102105
condition?: CatalogSubBlock['condition']
103106
}
104107

@@ -357,10 +360,15 @@ export function projectBlockTriggers(block: BlockConfig): CatalogBlockTrigger[]
357360
field.options = subBlock.options.map((option) => ({
358361
id: option.id,
359362
label: option.label || option.id,
363+
...(option.reactiveCondition
364+
? { reactiveCondition: copyCredentialCondition(option.reactiveCondition) }
365+
: {}),
360366
}))
361367
}
362368
const condition = normalizeCondition(subBlock.condition)
363369
if (condition) field.condition = condition
370+
if (subBlock.reactiveCondition)
371+
field.reactiveCondition = copyCredentialCondition(subBlock.reactiveCondition)
364372

365373
configFields[subBlock.id] = field
366374
}

‎apps/sim/lib/catalog/projection/catalog-sweep.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,21 @@ function expectSerializable(projection: unknown, label: string): void {
106106
walk(projection, label)
107107
}
108108

109+
it('preserves credential gates in the Slack trigger contract sent to clients', () => {
110+
const config = getBlockRegistry().slack_v2
111+
const wire = v2BlockDetailSchema.parse(projectBlockDetail(config, { deployment: HOSTED }))
112+
const trigger = wire.triggers.find(({ id }) => id === 'slack_oauth')!
113+
expect(trigger.configFields.streamResponse).toMatchObject({
114+
reactiveCondition: { watchFields: ['customBotCredential'], requiredType: 'service_account' },
115+
condition: { field: 'eventType', value: ['message', 'app_mention'] },
116+
})
117+
expect(
118+
trigger.configFields.eventType.options?.find(({ id }) => id === 'assistant_thread_started')
119+
).toMatchObject({
120+
reactiveCondition: { watchFields: ['customBotCredential'], requiredType: 'oauth' },
121+
})
122+
})
123+
109124
describe('block catalog projection sweep', () => {
110125
const blocks = Object.values(getBlockRegistry())
111126

‎apps/sim/lib/catalog/projection/subblock.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,18 @@ import { useProvidersStore } from '@/stores/providers'
1212
* never describe the same field differently.
1313
*/
1414

15+
/** Credential-kind gate authored by the block, preserved for non-editor clients. */
16+
export type CatalogCredentialCondition = NonNullable<SubBlockConfig['reactiveCondition']>
17+
18+
export function copyCredentialCondition(
19+
condition: CatalogCredentialCondition
20+
): CatalogCredentialCondition {
21+
return { watchFields: [...condition.watchFields], requiredType: condition.requiredType }
22+
}
23+
1524
/** One selectable option on a dropdown, combobox, or multi-select field. */
1625
export interface CatalogSubBlockOption {
26+
reactiveCondition?: CatalogCredentialCondition
1727
id: string
1828
label?: string
1929
/** Whether the option renders with an icon. The icon component itself is never published. */
@@ -66,6 +76,7 @@ export interface CatalogSubBlock {
6676
hidden?: boolean
6777
/** The condition under which the field applies at all. */
6878
condition?: CatalogCondition
79+
reactiveCondition?: CatalogCredentialCondition
6980
options?: CatalogSubBlockOption[]
7081
min?: number
7182
max?: number
@@ -316,6 +327,8 @@ export function resolveSubBlockOptions(
316327
const projected: CatalogSubBlockOption = { id: String(option.id) }
317328
if (typeof option.label === 'string') projected.label = option.label
318329
if (option.icon) projected.hasIcon = true
330+
if (option.reactiveCondition)
331+
projected.reactiveCondition = copyCredentialCondition(option.reactiveCondition)
319332
normalized.push(projected)
320333
}
321334

@@ -352,6 +365,8 @@ export function projectSubBlock(subBlock: SubBlockConfig): CatalogSubBlock {
352365
assignDefined(projected, 'placeholder', subBlock.placeholder)
353366
assignDefined(projected, 'mode', subBlock.mode)
354367
assignDefined(projected, 'hidden', subBlock.hidden)
368+
if (subBlock.reactiveCondition)
369+
projected.reactiveCondition = copyCredentialCondition(subBlock.reactiveCondition)
355370
assignDefined(projected, 'canonicalParamId', subBlock.canonicalParamId)
356371
assignDefined(projected, 'defaultValue', subBlock.defaultValue)
357372
assignDefined(projected, 'min', subBlock.min)

‎apps/sim/lib/webhooks/slack-stream-config.test.ts‎

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -66,26 +66,29 @@ describe('Slack stream response config', () => {
6666
).toBe('Running')
6767
})
6868

69-
it('rejects non-reply events and malformed output selectors', () => {
70-
expect(() =>
71-
normalizeSlackStreamResponseConfig(
72-
{
73-
eventType: 'reaction_added',
74-
streamResponse: true,
75-
streamOutputs: ['block.content'],
76-
},
77-
{}
78-
)
79-
).toThrow('reply-capable')
80-
expect(() =>
81-
normalizeSlackStreamResponseConfig(
82-
{
83-
eventType: 'message',
84-
streamResponse: true,
85-
streamOutputs: ['block_content'],
86-
},
87-
{}
88-
)
89-
).toThrow('Invalid Slack stream output selector')
90-
})
69+
it.each(['reaction_added', 'assistant_thread_started'])(
70+
'rejects unsupported streaming event %s and malformed output selectors',
71+
(eventType) => {
72+
expect(() =>
73+
normalizeSlackStreamResponseConfig(
74+
{
75+
eventType,
76+
streamResponse: true,
77+
streamOutputs: ['block.content'],
78+
},
79+
{}
80+
)
81+
).toThrow('reply-capable')
82+
expect(() =>
83+
normalizeSlackStreamResponseConfig(
84+
{
85+
eventType: 'message',
86+
streamResponse: true,
87+
streamOutputs: ['block_content'],
88+
},
89+
{}
90+
)
91+
).toThrow('Invalid Slack stream output selector')
92+
}
93+
)
9194
})

‎apps/sim/lib/webhooks/slack-stream-config.ts‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@ import {
55
resolveOutputBlockRef,
66
} from '@/lib/workflows/streaming/output-selector'
77
import { normalizeName } from '@/executor/constants'
8-
9-
export const SLACK_STREAM_RESPONSE_EVENTS = [
10-
'message',
11-
'app_mention',
12-
'assistant_thread_started',
13-
] as const
8+
import { SLACK_STREAM_RESPONSE_EVENTS } from '@/triggers/slack/shared'
149

1510
export interface SlackStreamOutputConfig {
1611
workflowId?: string

0 commit comments

Comments
 (0)