Skip to content

Commit a920973

Browse files
committed
fix(search): catch variable-bound dynamic imports in the dormant boundary audit and use sleep for the probe wait
1 parent d327e5d commit a920973

3 files changed

Lines changed: 30 additions & 9 deletions

File tree

‎apps/sim/lib/sim-search/indexed/retrieval/projection-fill.ts‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { SOURCE_ACL_PROJECTIONS, type SourceAclProjection } from '@sim/db/knowledge-projection'
22
import { embeddingKeywordTin, embeddingSearch } from '@sim/db/schema'
3+
import { sleep } from '@sim/utils/helpers'
34
import { sql } from 'drizzle-orm'
45
import { LRUCache } from 'lru-cache'
56
import { runSearchQuery, type SearchBudget } from '@/lib/knowledge/search/budget'
@@ -88,15 +89,8 @@ export async function isProjectionFilled(
8889
0,
8990
Math.min(PROJECTION_FILLED_PROBE_BUDGET_MS, budget.deadline - performance.now())
9091
)
91-
let timer: ReturnType<typeof setTimeout> | undefined
92-
const unanswered = new Promise<undefined>((resolve) => {
93-
timer = setTimeout(resolve, waitMs, undefined)
94-
})
95-
try {
96-
return (await Promise.race([answer.catch(() => undefined), unanswered])) ?? false
97-
} finally {
98-
clearTimeout(timer)
99-
}
92+
const unanswered = sleep(waitMs).then(() => undefined)
93+
return (await Promise.race([answer.catch(() => undefined), unanswered])) ?? false
10094
}
10195

10296
/** Forgets whether the projections were filled; the memo is per process and otherwise expires on its own. */

‎scripts/check-indexed-org-search-boundary.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,23 @@ describe('indexed organization search boundary audit', () => {
104104
])
105105
})
106106

107+
it('catches a dynamic import whose target is held in a variable', () => {
108+
expect(
109+
findBoundaryViolations([
110+
{
111+
file: 'apps/sim/lib/other/loader.ts',
112+
source: `const target = '${USE_CASE_BARREL}'\nexport const load = () => import(target)`,
113+
},
114+
])
115+
).toEqual([
116+
expect.objectContaining({
117+
file: 'apps/sim/lib/other/loader.ts',
118+
specifier: USE_CASE_BARREL,
119+
reason: `is not an allowlisted entry point for ${USE_CASE_BARREL}`,
120+
}),
121+
])
122+
})
123+
107124
it('accepts a gate called under an aliased import', () => {
108125
expect(
109126
findBoundaryViolations([

‎scripts/check-indexed-org-search-boundary.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,30 @@ export function moduleSpecifiers(
9292
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
9393
found.push({ specifier, line: line + 1 })
9494
}
95+
const consumed = new Set<ts.Node>()
9596
const visit = (node: ts.Node): void => {
9697
if (
9798
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
9899
node.moduleSpecifier &&
99100
ts.isStringLiteralLike(node.moduleSpecifier)
100101
) {
101102
record(node, node.moduleSpecifier.text)
103+
consumed.add(node.moduleSpecifier)
102104
} else if (ts.isCallExpression(node)) {
103105
const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword
104106
const isRequire = ts.isIdentifier(node.expression) && node.expression.text === 'require'
105107
const argument = node.arguments[0]
106108
if ((isDynamicImport || isRequire) && argument && ts.isStringLiteralLike(argument)) {
107109
record(node, argument.text)
110+
consumed.add(argument)
108111
}
112+
} else if (ts.isStringLiteralLike(node) && !consumed.has(node)) {
113+
/**
114+
* Any other string naming a module is a specifier too: a dynamic import or `require` of a
115+
* variable is only as safe as the strings that variable can hold, so the string is where
116+
* the reach into the dormant directory is caught.
117+
*/
118+
record(node, node.text)
109119
}
110120
ts.forEachChild(node, visit)
111121
}

0 commit comments

Comments
 (0)