diff --git a/apps/sim/lib/mothership/chat/delegation.test.ts b/apps/sim/lib/mothership/chat/delegation.test.ts index 5096228b90f..b28278d50be 100644 --- a/apps/sim/lib/mothership/chat/delegation.test.ts +++ b/apps/sim/lib/mothership/chat/delegation.test.ts @@ -29,6 +29,7 @@ interface KeyRow { name: string key: string expiresAt: Date + access?: 'read' | 'write' } /** Models a transaction-scoped advisory lock and commits only after the entire callback succeeds. */ @@ -64,12 +65,18 @@ function database() { if (!locked) throw new Error('Read before acquiring the user lock') return { from: () => ({ - where: () => ({ - limit: async () => { - const row = rows.get(name) - return row && row.expiresAt.getTime() > Date.now() + 30 * 60_000 ? [row] : [] - }, - }), + where: (condition: SQL) => { + const query = dialect.sqlToQuery(condition) + // Only the access filter binds 'write'; the other predicates bind ids, names and dates. + const writeOnly = query.params.includes('write') + return { + limit: async () => { + const row = rows.get(name) + if (!row || row.expiresAt.getTime() <= Date.now() + 30 * 60_000) return [] + return writeOnly && row.access === 'read' ? [] : [row] + }, + } + }, }), } }, @@ -154,6 +161,22 @@ describe('delegation key concurrency', () => { expect(db.rows.get('mothership-delegation:alice')).toBe(old) }) + it('replaces a read-only key instead of handing it to the CLI', async () => { + const db = database() + db.rows.set('mothership-delegation:actor', { + id: 'read-key', + name: 'mothership-delegation:actor', + key: 'encrypted-read-only', + expiresAt: new Date(Date.now() + 6 * 60 * 60_000), + access: 'read', + }) + expect(await mintDelegationToken({ workspaceId: 'shared', userId: 'actor' })).toBe('plain-1') + expect(db.rows.get('mothership-delegation:actor')).toMatchObject({ + key: 'encrypted-1', + access: 'write', + }) + }) + it('does not log bound credential values from database errors', async () => { mocks.transaction.mockRejectedValue( new Error('Failed query params: plaintext-key, encrypted-key') diff --git a/apps/sim/lib/mothership/chat/delegation.ts b/apps/sim/lib/mothership/chat/delegation.ts index 18b46064da2..80e313e3ab9 100644 --- a/apps/sim/lib/mothership/chat/delegation.ts +++ b/apps/sim/lib/mothership/chat/delegation.ts @@ -49,6 +49,7 @@ export async function mintDelegationToken(params: { eq(apiKey.userId, params.userId), eq(apiKey.name, name), eq(apiKey.type, 'personal'), + eq(apiKey.access, 'write'), gt(apiKey.expiresAt, new Date(Date.now() + MIN_REMAINING_MS)) ) ) @@ -83,6 +84,7 @@ export async function mintDelegationToken(params: { key: stored, keyHash: hashApiKey(plainKey), type: 'personal', + access: 'write', createdAt: now, updatedAt: now, expiresAt, diff --git a/apps/sim/tools/firecrawl/crawl.test.ts b/apps/sim/tools/firecrawl/crawl.test.ts index 5c4119c60cc..2b342396d14 100644 --- a/apps/sim/tools/firecrawl/crawl.test.ts +++ b/apps/sim/tools/firecrawl/crawl.test.ts @@ -1,6 +1,7 @@ import { isPlainRecord } from '@sim/utils/object' import { describe, expect, it } from 'vitest' import { crawlTool } from '@/tools/firecrawl/crawl' +import type { FirecrawlCrawlParams } from '@/tools/firecrawl/types' function crawlBody(options: { maxDepth: number @@ -98,4 +99,17 @@ describe('Firecrawl legacy crawl depth', () => { expect(() => crawlBody({ maxDepth })).toThrow(/maxDepth/) } ) + + it('treats a blank depth as unset instead of a zero-depth crawl', () => { + if (typeof crawlTool.request.body !== 'function') throw new Error('Missing request formatter') + const blankDepth: FirecrawlCrawlParams = { + apiKey: 'test', + url: 'https://example.com/docs', + maxDepth: '', + } + const body = crawlTool.request.body(blankDepth) + if (!isPlainRecord(body)) throw new Error('Expected a JSON request body') + expect(body.includePaths).toBeUndefined() + expect(body.regexOnFullURL).toBeUndefined() + }) }) diff --git a/apps/sim/tools/firecrawl/crawl.ts b/apps/sim/tools/firecrawl/crawl.ts index 3859fdb4975..110b4529b76 100644 --- a/apps/sim/tools/firecrawl/crawl.ts +++ b/apps/sim/tools/firecrawl/crawl.ts @@ -168,7 +168,7 @@ export const crawlTool: ToolConfig if (params.maxConcurrency) body.maxConcurrency = Number(params.maxConcurrency) if (params.excludePaths) body.excludePaths = params.excludePaths if (params.includePaths) body.includePaths = params.includePaths - if (params.maxDepth != null) { + if (params.maxDepth != null && String(params.maxDepth).trim() !== '') { const requestedDepth = Number(params.maxDepth) if (!Number.isSafeInteger(requestedDepth) || requestedDepth < 0) { throw new Error('maxDepth must be a non-negative integer') diff --git a/apps/sim/tools/firecrawl/types.ts b/apps/sim/tools/firecrawl/types.ts index 60dd60c3ef7..571909aadf9 100644 --- a/apps/sim/tools/firecrawl/types.ts +++ b/apps/sim/tools/firecrawl/types.ts @@ -285,7 +285,8 @@ export interface FirecrawlCrawlParams { apiKey: string url: string limit?: number - maxDepth?: number + /** Saved tool params can carry the raw field text, so a cleared field arrives as `''`. */ + maxDepth?: number | string formats?: FirecrawlFormat[] onlyMainContent?: boolean prompt?: string