diff --git a/apps/sim/app/api/table/[tableId]/route.test.ts b/apps/sim/app/api/table/[tableId]/route.test.ts index 2396ba13a21..43cbf68ae83 100644 --- a/apps/sim/app/api/table/[tableId]/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/route.test.ts @@ -55,7 +55,7 @@ vi.mock('@/app/api/table/utils', () => ({ tableLockErrorResponse: () => null, })) -import { PATCH } from '@/app/api/table/[tableId]/route' +import { GET, PATCH } from '@/app/api/table/[tableId]/route' const TABLE = { id: 'tbl_1', @@ -162,3 +162,47 @@ describe('PATCH /api/table/[tableId] folder moves', () => { expect(mockRenameTable).not.toHaveBeenCalled() }) }) + +/** + * Pins which auth path this route hands a Bearer token to. + * + * `fetchTableSchema` in `@/tools/schema-enrichers` reaches this route with a legacy + * `type: 'internal'` token from the deprecated `buildAuthHeaders`. Only + * `checkSessionOrInternalAuth` accepts that token; the delegation policy the sibling + * table routes use rejects it outright. Migrating this route without moving that caller + * to `buildExecutorDelegationHeaders` in the same change breaks every table tool on an + * Agent block, so this fails first and names the caller. + * + * Scope: this pins the *route's* choice of verifier. That the legacy token is actually + * valid for that verifier — and rejected by the delegation one — is pinned separately in + * `@/lib/auth/internal.test.ts`. Both halves are needed; neither implies the other. + */ +describe('GET /api/table/[tableId] executor auth pairing', () => { + beforeEach(() => { + vi.clearAllMocks() + hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({ + success: true, + userId: 'user-1', + authType: 'internal_jwt', + }) + mockCheckAccess.mockResolvedValue({ ok: true, table: TABLE }) + mockGetLimits.mockResolvedValue({ maxRowsPerTable: 1000 }) + }) + + it('routes a Bearer token to the legacy verifier fetchTableSchema mints for', async () => { + const request = new NextRequest('http://localhost:3000/api/table/tbl_1?workspaceId=workspace-1') + request.headers.set('authorization', 'Bearer legacy-internal-token') + + const response = await GET(request, routeContext) + + expect(response.status).toBe(200) + expect(hybridAuthMockFns.mockCheckSessionOrInternalAuth).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ get: expect.any(Function) }), + }), + expect.anything() + ) + const [forwarded] = hybridAuthMockFns.mockCheckSessionOrInternalAuth.mock.calls[0] + expect(forwarded.headers.get('authorization')).toBe('Bearer legacy-internal-token') + }) +}) diff --git a/apps/sim/tools/schema-enrichers.ts b/apps/sim/tools/schema-enrichers.ts index 9c2bd7abe31..8b4ea246369 100644 --- a/apps/sim/tools/schema-enrichers.ts +++ b/apps/sim/tools/schema-enrichers.ts @@ -7,6 +7,19 @@ import type { WorkflowToolExecutionContext } from '@/tools/types' const logger = createLogger('SchemaEnrichers') +/** + * Reads a table's schema as the acting user. + * + * Deliberately still on the deprecated `buildAuthHeaders`, unlike its siblings in this + * file: `GET /api/table/[tableId]` authenticates through `checkSessionOrInternalAuth`, + * which accepts a legacy `type: 'internal'` token and rejects an executor delegation. + * Swapping this to `buildExecutorDelegationHeaders` before that route migrates would + * break every table tool on an Agent block. The route's own test pins the pairing. + * + * Unlike the workflow and knowledge enrichers, a failure here is loud — this runs as a + * tool-level `toolEnrichment`, so `createLLMToolSchema` surfaces it as a + * `ToolSchemaEnrichmentError` naming the tool rather than degrading the schema silently. + */ async function fetchTableSchema( tableId: string, context: WorkflowToolExecutionContext