From 5e7bf97b44090c6b28f6f7aaa91d196ad192615b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 11 Aug 2026 21:59:09 -0700 Subject: [PATCH 1/2] test(table): pin the executor auth pairing on the table read route fetchTableSchema reaches GET /api/table/[tableId] with a legacy type:'internal' token, which only works while that route authenticates through checkSessionOrInternalAuth. Its sibling table routes already moved to the delegation policy, which rejects that token outright, so migrating this one without moving the caller in the same change would break every table tool on an Agent block. Assert the route still authenticates through the legacy path so that migration fails here first, and record on the caller why it is deliberately not on buildExecutorDelegationHeaders yet. --- .../sim/app/api/table/[tableId]/route.test.ts | 35 ++++++++++++++++++- apps/sim/tools/schema-enrichers.ts | 13 +++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/api/table/[tableId]/route.test.ts b/apps/sim/app/api/table/[tableId]/route.test.ts index 2396ba13a21..a5b6a46bec4 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,36 @@ describe('PATCH /api/table/[tableId] folder moves', () => { expect(mockRenameTable).not.toHaveBeenCalled() }) }) + +/** + * Pins the auth mode this route's executor caller is built against. + * + * `fetchTableSchema` in `@/tools/schema-enrichers` reaches this route with a legacy + * `type: 'internal'` token from the deprecated `buildAuthHeaders`. That token is only + * accepted while GET authenticates through `checkSessionOrInternalAuth`; 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 attached to an Agent block, so this assertion fails first and says so. + */ +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('authenticates a legacy internal token, which is what fetchTableSchema sends', async () => { + const response = await GET( + new NextRequest('http://localhost:3000/api/table/tbl_1?workspaceId=workspace-1'), + routeContext + ) + + expect(response.status).toBe(200) + expect(hybridAuthMockFns.mockCheckSessionOrInternalAuth).toHaveBeenCalled() + }) +}) 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 From 2c3fc064631d3bd9345b845da9998427a4909e53 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 11 Aug 2026 22:04:06 -0700 Subject: [PATCH 2/2] test(table): assert the Bearer header reaches the legacy verifier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review: the pairing test sent no Authorization header and its name claimed to verify token acceptance, which is pinned separately in lib/auth/internal.test.ts. Send a representative header, assert it reaches checkSessionOrInternalAuth unmodified, and scope the name and docs to what this guard actually covers — the route's choice of verifier. --- .../sim/app/api/table/[tableId]/route.test.ts | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/apps/sim/app/api/table/[tableId]/route.test.ts b/apps/sim/app/api/table/[tableId]/route.test.ts index a5b6a46bec4..43cbf68ae83 100644 --- a/apps/sim/app/api/table/[tableId]/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/route.test.ts @@ -164,14 +164,18 @@ describe('PATCH /api/table/[tableId] folder moves', () => { }) /** - * Pins the auth mode this route's executor caller is built against. + * 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`. That token is only - * accepted while GET authenticates through `checkSessionOrInternalAuth`; 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 attached to an Agent block, so this assertion fails first and says so. + * `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(() => { @@ -185,13 +189,20 @@ describe('GET /api/table/[tableId] executor auth pairing', () => { mockGetLimits.mockResolvedValue({ maxRowsPerTable: 1000 }) }) - it('authenticates a legacy internal token, which is what fetchTableSchema sends', async () => { - const response = await GET( - new NextRequest('http://localhost:3000/api/table/tbl_1?workspaceId=workspace-1'), - routeContext - ) + 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).toHaveBeenCalled() + 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') }) })