diff --git a/tests/evals/graders/__tests__/react-provider-mount.spec.ts b/tests/evals/graders/__tests__/react-provider-mount.spec.ts
new file mode 100644
index 00000000..4da467f3
--- /dev/null
+++ b/tests/evals/graders/__tests__/react-provider-mount.spec.ts
@@ -0,0 +1,52 @@
+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 { findMountedAuthKitProvider } from '../react.grader.js';
+
+const PROVIDER = `import { AuthKitProvider } from '@workos-inc/authkit-react';
+export function AuthProvider({ children }) {
+ return {children};
+}
+`;
+
+describe('findMountedAuthKitProvider', () => {
+ let workDir: string;
+
+ beforeEach(async () => {
+ workDir = await mkdtemp(join(tmpdir(), 'react-grader-'));
+ await mkdir(join(workDir, 'src/auth'), { recursive: true });
+ });
+
+ afterEach(async () => {
+ await rm(workDir, { recursive: true, force: true });
+ });
+
+ it('accepts AuthKitProvider in the entry file', async () => {
+ await writeFile(join(workDir, 'src/main.tsx'), PROVIDER);
+ expect(await findMountedAuthKitProvider(workDir)).toBe('src/main.tsx');
+ });
+
+ it('accepts a provider module imported by the entry file', async () => {
+ await writeFile(join(workDir, 'src/auth/AuthProvider.tsx'), PROVIDER);
+ await writeFile(join(workDir, 'src/main.tsx'), `import { AuthProvider } from './auth/AuthProvider';\n`);
+ expect(await findMountedAuthKitProvider(workDir)).toBe('src/auth/AuthProvider.tsx');
+ });
+
+ it('accepts a provider module imported via the @/ alias from another component', async () => {
+ await writeFile(join(workDir, 'src/auth/index.tsx'), PROVIDER);
+ await writeFile(join(workDir, 'src/App.tsx'), `import { AuthProvider } from '@/auth';\n`);
+ expect(await findMountedAuthKitProvider(workDir)).toBe('src/auth/index.tsx');
+ });
+
+ it('rejects a provider module nothing imports', async () => {
+ await writeFile(join(workDir, 'src/auth/AuthProvider.tsx'), PROVIDER);
+ await writeFile(join(workDir, 'src/main.tsx'), `import App from './App';\n`);
+ expect(await findMountedAuthKitProvider(workDir)).toBeNull();
+ });
+
+ it('rejects a provider from a different SDK', async () => {
+ await writeFile(join(workDir, 'src/main.tsx'), `import { AuthKitProvider } from '@workos-inc/authkit-js';\n`);
+ expect(await findMountedAuthKitProvider(workDir)).toBeNull();
+ });
+});
diff --git a/tests/evals/graders/nextjs.grader.ts b/tests/evals/graders/nextjs.grader.ts
index 6caf7374..f4c3fd74 100644
--- a/tests/evals/graders/nextjs.grader.ts
+++ b/tests/evals/graders/nextjs.grader.ts
@@ -113,11 +113,11 @@ export class NextjsGrader implements Grader {
const sdkImportChecks = await this.fileGrader.checkFileContains(middlewareFile, ['@workos-inc/authkit-nextjs']);
checks.push(...sdkImportChecks);
- // Check for authkit integration: authkitMiddleware OR (authkit + handleAuthkitHeaders)
+ // Check for authkit integration: authkitMiddleware OR (authkit + handleAuthkitProxy, or its deprecated alias handleAuthkitHeaders)
const middlewareChecks = await this.fileGrader.checkFileContains(middlewareFile, ['authkitMiddleware']);
const composableChecks = await this.fileGrader.checkFileContains(middlewareFile, [
'authkit(',
- 'handleAuthkitHeaders',
+ /handleAuthkit(Proxy|Headers)\b/,
]);
const usesAuthkitMiddleware = middlewareChecks.every((c) => c.passed);
@@ -129,7 +129,7 @@ export class NextjsGrader implements Grader {
message: usesAuthkitMiddleware
? 'Uses authkitMiddleware'
: usesComposable
- ? 'Uses authkit() composable with handleAuthkitHeaders'
+ ? 'Uses authkit() composable with handleAuthkitProxy'
: 'Missing authkitMiddleware or authkit() composable integration',
};
checks.push(authkitCheck);
diff --git a/tests/evals/graders/react.grader.ts b/tests/evals/graders/react.grader.ts
index 86779a67..2f47c138 100644
--- a/tests/evals/graders/react.grader.ts
+++ b/tests/evals/graders/react.grader.ts
@@ -1,3 +1,6 @@
+import fg from 'fast-glob';
+import { readFile } from 'node:fs/promises';
+import { dirname, join, relative, resolve } from 'node:path';
import { FileGrader } from './file-grader.js';
import { BuildGrader } from './build-grader.js';
import type { Grader, GradeResult, GradeCheck } from '../types.js';
@@ -18,7 +21,7 @@ export class ReactGrader implements Grader {
private fileGrader: FileGrader;
private buildGrader: BuildGrader;
- constructor(workDir: string) {
+ constructor(private workDir: string) {
this.fileGrader = new FileGrader(workDir);
this.buildGrader = new BuildGrader(workDir);
}
@@ -26,15 +29,15 @@ export class ReactGrader implements Grader {
async grade(): Promise {
const checks: GradeCheck[] = [];
- // Check AuthKitProvider wrapper exists in entry file
- // Can be main.tsx (Vite) or index.tsx (CRA)
- checks.push(
- await this.fileGrader.checkFileWithPattern(
- 'src/{main,index}.tsx',
- ['AuthKitProvider', '@workos-inc/authkit-react'],
- 'AuthKitProvider configured with correct SDK',
- ),
- );
+ // AuthKitProvider may live in the entry file or an app-owned provider module, but it must be mounted
+ const provider = await findMountedAuthKitProvider(this.workDir);
+ checks.push({
+ name: 'AuthKitProvider configured with correct SDK',
+ passed: provider !== null,
+ message: provider
+ ? `Found in: ${provider}`
+ : 'No mounted AuthKitProvider from @workos-inc/authkit-react (entry file or a module imported elsewhere in src/)',
+ });
// Check useAuth hook usage somewhere in the app
// Can be in App.tsx, pages/, components/, or anywhere
@@ -46,12 +49,11 @@ export class ReactGrader implements Grader {
),
);
- // Check environment config in entry file
- // Supports both Vite (import.meta.env) and CRA (process.env)
+ // Client ID comes from build-time env (Vite or CRA), read anywhere under src/
checks.push(
await this.fileGrader.checkFileWithPattern(
- 'src/{main,index}.tsx',
- [/VITE_WORKOS_CLIENT_ID|REACT_APP_WORKOS_CLIENT_ID|import\.meta\.env|process\.env/],
+ 'src/**/*.{ts,tsx,js,jsx}',
+ [/(VITE|REACT_APP)_WORKOS_CLIENT_ID/],
'Environment variable configuration',
),
);
@@ -65,3 +67,37 @@ export class ReactGrader implements Grader {
};
}
}
+
+const SOURCE_EXT = /\.(tsx|ts|jsx|js)$/;
+const IMPORT_SPEC = /(?:from\s+|import\s*\(\s*|import\s+)['"]([^'"]+)['"]/g;
+
+/**
+ * Returns the src-relative file that defines AuthKitProvider when it is mounted:
+ * either it is the entry file, or another src file imports its module.
+ */
+export async function findMountedAuthKitProvider(workDir: string): Promise {
+ const files = await fg('src/**/*.{tsx,ts,jsx,js}', { cwd: workDir });
+ const contents = new Map();
+ for (const file of files) contents.set(file, await readFile(join(workDir, file), 'utf-8'));
+
+ const providers = files.filter((f) => {
+ const c = contents.get(f)!;
+ return c.includes('AuthKitProvider') && c.includes('@workos-inc/authkit-react');
+ });
+
+ for (const provider of providers) {
+ if (/^src\/(main|index)\.(tsx|jsx)$/.test(provider)) return provider;
+ const target = provider.replace(SOURCE_EXT, '').replace(/\/index$/, '');
+ for (const [importer, content] of contents) {
+ if (importer === provider) continue;
+ for (const [, spec] of content.matchAll(IMPORT_SPEC)) {
+ let resolved: string | null = null;
+ if (spec.startsWith('.')) resolved = relative(workDir, resolve(workDir, dirname(importer), spec));
+ else if (spec.startsWith('@/')) resolved = join('src', spec.slice(2));
+ else if (spec.startsWith('src/')) resolved = spec;
+ if (resolved && resolved.replace(SOURCE_EXT, '').replace(/\/index$/, '') === target) return provider;
+ }
+ }
+ }
+ return null;
+}