Skip to content

Commit 82a80c3

Browse files
icecrasher321claude
andcommitted
fix(executor): keep a caller from choosing which secret expands
A placeholder-bearing string stays in source so the boundary compiler can expand it, which is how a workflow variable holding `{{NAME}}` reaches its value. Any run value took that path too, so text arriving from a trigger or a loop item could name a secret and have the compiler materialize it beside the payload that named it. Only a workflow variable — a surface an author configures — keeps the inline form now; every other value binds. A block comment can also stand between a property-access dot and a keyword-named method, hiding the dot from the control-flow-head check. A comment ending there now reads as the method call it almost always is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 4fb3027 commit 82a80c3

2 files changed

Lines changed: 38 additions & 3 deletions

File tree

apps/sim/executor/variables/resolver.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,28 @@ describe('VariableResolver function block inputs', () => {
964964
)
965965
})
966966

967+
it('binds a run value that names a secret instead of expanding it', async () => {
968+
const { block, ctx, resolver } = createResolver('javascript')
969+
ctx.workflowVariables = {
970+
'var-1': { id: 'var-1', name: 'authTemplate', type: 'string', value: 'Bearer {{API_KEY}}' },
971+
}
972+
973+
const result = await resolver.resolveInputsForFunctionBlock(
974+
ctx,
975+
'function',
976+
{ code: `const header = '<variable.authTemplate>'; const item = <producer.result>` },
977+
block
978+
)
979+
980+
// An author-configured variable keeps its placeholder in source, where the boundary
981+
// compiler expands it; a run value naming a secret binds instead, so whoever supplies
982+
// the text cannot pick what the compiler materializes next to it.
983+
const code = result.resolvedInputs.code as string
984+
expect(code).toContain('{{API_KEY}}')
985+
expect(code).toContain('const item = globalThis["__blockRef_0"]')
986+
expect(result.contextVariables).toEqual({ __blockRef_0: 'hello world' })
987+
})
988+
967989
it('binds a workflow variable carrying quote characters instead of splicing it into code', async () => {
968990
// A Variables block can assign trigger data at runtime, so a variable's value is not
969991
// necessarily the author's. Inlined as a literal it closed the string it landed in.

apps/sim/executor/variables/resolver.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,7 @@ export class VariableResolver {
687687
throw getNestedLargeValueMaterializationError()
688688
}
689689

690-
if (this.canInlineResolvedCodeLiteral(effectiveValue)) {
690+
if (this.canInlineResolvedCodeLiteral(effectiveValue, match)) {
691691
const replacement = this.blockResolver.formatValueForBlock(
692692
effectiveValue,
693693
BlockType.FUNCTION,
@@ -976,14 +976,21 @@ export class VariableResolver {
976976
* Everything else binds, which is what block outputs have always done. Inlining the rest
977977
* is what let a runtime-assigned variable or loop item carrying trigger data close the
978978
* string it landed in and run as code.
979+
*
980+
* The placeholder case is admitted only for a workflow variable, never for a loop item or
981+
* any other run value. Whoever supplies the text picks which secret the compiler expands
982+
* into the generated source, so that choice stays with the surface an author configures.
979983
*/
980-
private canInlineResolvedCodeLiteral(value: unknown): boolean {
984+
private canInlineResolvedCodeLiteral(value: unknown, reference: string): boolean {
981985
if (value === null || typeof value === 'number' || typeof value === 'boolean') {
982986
return true
983987
}
984988
if (typeof value !== 'string') {
985989
return false
986990
}
991+
if (parseReferencePath(reference)[0] !== REFERENCE.PREFIX.VARIABLE) {
992+
return false
993+
}
987994
return createEnvVarPattern().test(value) && !/['"`$\\\n\r\u2028\u2029]/.test(value)
988995
}
989996

@@ -1196,11 +1203,17 @@ export class VariableResolver {
11961203
}
11971204

11981205
// `p.catch(fn)` is a method call whose name happens to be a keyword, and what follows its
1199-
// `)` is an operator, not a statement. A control-flow head can never be a property access.
1206+
// `)` is an operator, not a statement. A control-flow head can never be a property access,
1207+
// and a comment can hide the dot (`p./* c */catch(fn)`), so a comment ending here is read
1208+
// as the method call it usually is: the wrong guess there costs a division scanned as a
1209+
// regex, while this way it costs nothing a regex-free line would notice.
12001210
let before = start
12011211
while (before > 0 && WHITESPACE_CHAR.test(template[before - 1])) {
12021212
before--
12031213
}
1214+
if (template[before - 1] === '/' && template[before - 2] === '*') {
1215+
return false
1216+
}
12041217
return template[before - 1] !== '.'
12051218
}
12061219

0 commit comments

Comments
 (0)