diff --git a/package.json b/package.json index e343b0f6..564e0c92 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@clack/prompts": "1.0.1", "@napi-rs/keyring": "^1.2.0", "@workos-inc/node": "^8.7.0", - "@workos/skills": "0.2.2", + "@workos/skills": "0.2.4", "chalk": "^5.6.2", "diff": "^8.0.3", "fast-glob": "^3.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a158695e..5d682172 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: ^8.7.0 version: 8.7.0 '@workos/skills': - specifier: 0.2.2 - version: 0.2.2 + specifier: 0.2.4 + version: 0.2.4 chalk: specifier: ^5.6.2 version: 5.6.2 @@ -1601,8 +1601,8 @@ packages: resolution: {integrity: sha512-43HfXSR2Ez7M4ixpebuYVZzZf3gauh5jvv9lYnePg/x0XZMN2hjpEV3FD1LQX1vfMbqQ5gON3DN+/gH2rITm3A==} engines: {node: '>=20.15.0'} - '@workos/skills@0.2.2': - resolution: {integrity: sha512-jO4HuI4seyAjoBwfl2bTQe5FXE5ir1nyJ5+RkYG4yhpIvsIThBJkAoYJwdhRhFynzJAYuQIb7v6E7a2zBto7ww==} + '@workos/skills@0.2.4': + resolution: {integrity: sha512-Ku/8aD6zRwA7M4jREp/l1pUHOGCmDMBudlJAUfpkfZqFdDatxWJy+CQmYiIlMKt3IP4/GURe1rA72HpAcEkcaw==} abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} @@ -3833,7 +3833,7 @@ snapshots: iron-webcrypto: 2.0.0 jose: 6.1.3 - '@workos/skills@0.2.2': + '@workos/skills@0.2.4': dependencies: yaml: 2.8.2 diff --git a/tests/evals/graders/__tests__/nextjs-server-component-safety.spec.ts b/tests/evals/graders/__tests__/nextjs-server-component-safety.spec.ts new file mode 100644 index 00000000..3de42e00 --- /dev/null +++ b/tests/evals/graders/__tests__/nextjs-server-component-safety.spec.ts @@ -0,0 +1,138 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { findUnsafeGetSignInUrlUsage } from '../nextjs.grader.js'; + +describe('findUnsafeGetSignInUrlUsage', () => { + let workDir: string; + + beforeEach(async () => { + workDir = await mkdtemp(join(tmpdir(), 'grader-test-')); + await mkdir(join(workDir, 'app'), { recursive: true }); + }); + + afterEach(async () => { + await rm(workDir, { recursive: true, force: true }); + }); + + it('fails when getSignInUrl() is called in app/page.tsx', async () => { + await writeFile( + join(workDir, 'app/page.tsx'), + ` +import { getSignInUrl } from '@workos-inc/authkit-nextjs'; +export default async function Page() { + const url = await getSignInUrl(); + return Sign in; +} +`, + ); + const result = await findUnsafeGetSignInUrlUsage(workDir); + expect(result).not.toBeNull(); + expect(result!.file).toBe('app/page.tsx'); + }); + + it('fails when getSignInUrl() is in a shared component without directive', async () => { + await mkdir(join(workDir, 'app/components'), { recursive: true }); + await writeFile( + join(workDir, 'app/components/nav-auth.tsx'), + ` +import { getSignInUrl } from '@workos-inc/authkit-nextjs'; +export default async function NavAuth() { + const url = await getSignInUrl(); + return Sign in; +} +`, + ); + const result = await findUnsafeGetSignInUrlUsage(workDir); + expect(result).not.toBeNull(); + expect(result!.file).toContain('nav-auth.tsx'); + }); + + it('passes when getSignInUrl() is in a use client component', async () => { + await writeFile( + join(workDir, 'app/page.tsx'), + ` +'use client'; +import { getSignInUrl } from '@workos-inc/authkit-nextjs'; +export default function Page() { + const handleClick = async () => { const url = await getSignInUrl(); window.location.href = url; }; + return ; +} +`, + ); + const result = await findUnsafeGetSignInUrlUsage(workDir); + expect(result).toBeNull(); + }); + + it('passes when getSignInUrl() is in a top-level use server file', async () => { + await mkdir(join(workDir, 'app/actions'), { recursive: true }); + await writeFile( + join(workDir, 'app/actions/auth.tsx'), + ` +'use server'; +import { getSignInUrl } from '@workos-inc/authkit-nextjs'; +export async function getUrl() { return getSignInUrl(); } +`, + ); + const result = await findUnsafeGetSignInUrlUsage(workDir); + expect(result).toBeNull(); + }); + + it('fails when use server is inline, not top-level', async () => { + await writeFile( + join(workDir, 'app/page.tsx'), + ` +import { getSignInUrl } from '@workos-inc/authkit-nextjs'; +export default async function Page() { + const url = await getSignInUrl(); + async function logout() { + 'use server'; + // server action + } + return Sign in; +} +`, + ); + const result = await findUnsafeGetSignInUrlUsage(workDir); + expect(result).not.toBeNull(); + }); + + it('passes when no files contain getSignInUrl()', async () => { + await writeFile( + join(workDir, 'app/page.tsx'), + ` +export default function Page() { + return