Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 41 additions & 11 deletions skills/workos-authkit-nextjs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,39 @@ Detect package manager, install SDK package from README.

**Verify:** SDK package exists in node_modules before continuing.

## Step 4: Version Detection (Decision Tree)
## Step 4: Locate the app/ directory (BLOCKING)

**STOP. Do this before creating any files.**

Determine where the `app/` directory lives:

```bash
# Check for src/app/ first, then root app/
ls src/app/ 2>/dev/null && echo "APP_DIR=src" || (ls app/ 2>/dev/null && echo "APP_DIR=root")
```

Set `APP_DIR` for all subsequent steps. All middleware/proxy files MUST be created in `APP_DIR`:

- If `APP_DIR=src` → create files in `src/` (e.g., `src/proxy.ts`)
- If `APP_DIR=root` → create files at project root (e.g., `proxy.ts`)

Next.js only discovers middleware/proxy files in the parent directory of `app/`. A file at the wrong level is **silently ignored** — no error, just doesn't run.

## Step 5: Version Detection (Decision Tree)

Read Next.js version from `package.json`:

```
Next.js version?
|
+-- 16+ --> Create proxy.ts at project root
+-- 16+ --> Create {APP_DIR}/proxy.ts
|
+-- 15 --> Create middleware.ts (cookies() is async - handlers must await)
+-- 15 --> Create {APP_DIR}/middleware.ts (cookies() is async)
|
+-- 13-14 --> Create middleware.ts (cookies() is sync)
+-- 13-14 --> Create {APP_DIR}/middleware.ts (cookies() is sync)
```

**Critical:** File MUST be at project root (or `src/` if using src directory). Never in `app/`.
**Next.js 16+ proxy.ts:** `proxy.ts` is the preferred convention. `middleware.ts` still works but shows a deprecation warning. Next.js 16 throws **error E900** if both files exist at the same level.

**Next.js 15+ async note:** All route handlers and middleware accessing cookies must be async and properly await cookie operations. This is a breaking change from Next.js 14.

Expand Down Expand Up @@ -95,14 +113,14 @@ export default async function middleware(request: NextRequest) {

**Critical:** Always return via `handleAuthkitHeaders()` to ensure `withAuth()` works in pages.

## Step 5: Create Callback Route
## Step 6: Create Callback Route

Parse `NEXT_PUBLIC_WORKOS_REDIRECT_URI` to determine route path:

```
URI path --> Route location
/auth/callback --> app/auth/callback/route.ts
/callback --> app/callback/route.ts
URI path --> Route location (use APP_DIR from Step 4)
/auth/callback --> {APP_DIR}/app/auth/callback/route.ts
/callback --> {APP_DIR}/app/callback/route.ts
```

Use `handleAuth()` from SDK. Do not write custom OAuth logic.
Expand All @@ -118,7 +136,7 @@ export const GET = handleAuth();

Check README for exact usage. If build fails with "cookies outside request scope", the handler is likely missing async/await.

## Step 6: Provider Setup (REQUIRED)
## Step 7: Provider Setup (REQUIRED)

**CRITICAL:** You MUST wrap the app in `AuthKitProvider` in `app/layout.tsx`.

Expand Down Expand Up @@ -147,7 +165,7 @@ Check README for exact import path - it may be a subpath export like `@workos-in

**Do NOT skip this step** even if using server-side auth patterns elsewhere.

## Step 7: UI Integration
## Step 8: UI Integration

Add auth UI to `app/page.tsx` using SDK functions. See README for `getUser`, `getSignInUrl`, `signOut` usage.

Expand Down Expand Up @@ -191,6 +209,18 @@ This error causes OAuth codes to expire ("invalid_grant"), so fix the handler fi
- Check: File at project root or `src/`, not inside `app/`
- Check: Filename matches Next.js version (proxy.ts for 16+, middleware.ts for 13-15)

### "Both middleware file and proxy file are detected" (Next.js 16+)

- Next.js 16 throws error E900 if both `middleware.ts` and `proxy.ts` exist
- Delete `middleware.ts` and use only `proxy.ts`
- If `middleware.ts` has custom logic, migrate it into `proxy.ts`

### "withAuth route not covered by middleware" but middleware/proxy file exists

- **Most common cause:** File is at the wrong level. Next.js only discovers middleware/proxy files in the parent directory of `app/`. For `src/app/` projects, the file must be in `src/`, not at the project root.
- Check: Is `app/` at `src/app/`? Then middleware/proxy must be at `src/middleware.ts` or `src/proxy.ts`
- Check: Matcher config must include the route path being accessed

### "Cannot use getUser in client component"

- Check: Component has no `'use client'` directive, or
Expand Down
6 changes: 3 additions & 3 deletions src/lib/validation/rules/nextjs.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
],
"files": [
{
"path": "app/**/callback/**/route.{ts,tsx,js,jsx}",
"path": "{,src/}app/**/callback/**/route.{ts,tsx,js,jsx}",
"mustContain": ["handleAuth", "@workos-inc/authkit-nextjs"]
},
{
"path": "{middleware,proxy}.{ts,js}",
"path": "{,src/}{middleware,proxy}.{ts,js}",
"mustContainAny": ["authkitMiddleware", "authkit"]
},
{
"path": "app/**/layout.{ts,tsx,js,jsx}",
"path": "{,src/}app/**/layout.{ts,tsx,js,jsx}",
"mustContain": ["AuthKitProvider"]
}
]
Expand Down
2 changes: 1 addition & 1 deletion src/lib/validation/validator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ describe('validateInstallation', () => {

const middlewareIssue = result.issues.find((i) => i.message.includes('wrong location'));
expect(middlewareIssue).toBeDefined();
expect(middlewareIssue?.hint).toContain('project root');
expect(middlewareIssue?.hint).toContain('alongside app/ directory');
});

it('passes when middleware is at project root', async () => {
Expand Down
57 changes: 45 additions & 12 deletions src/lib/validation/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,30 +590,63 @@ async function validateCredentialFormats(projectDir: string, issues: ValidationI
}

/**
* Validates Next.js middleware.ts is at the correct location.
* Must be at project root or src/ folder, not nested deeper.
* Validates Next.js middleware/proxy is at the correct location.
* Must be alongside the app/ directory — Next.js only watches for these files
* in the parent directory of app/.
*/
async function validateNextjsMiddlewarePlacement(projectDir: string, issues: ValidationIssue[]): Promise<void> {
// Valid locations
const validPaths = ['middleware.ts', 'middleware.js', 'src/middleware.ts', 'src/middleware.js'];
// Determine where app/ lives to know where middleware/proxy should be
const appInSrc = existsSync(join(projectDir, 'src', 'app'));
const expectedDir = appInSrc ? 'src/' : '';

const correctPaths = [
`${expectedDir}middleware.ts`,
`${expectedDir}middleware.js`,
`${expectedDir}proxy.ts`,
`${expectedDir}proxy.js`,
];

const hasCorrectPlacement = correctPaths.some((p) => existsSync(join(projectDir, p)));
if (hasCorrectPlacement) {
return;
}

// Check for middleware/proxy at the wrong level
const allPossible = [
'middleware.ts',
'middleware.js',
'src/middleware.ts',
'src/middleware.js',
'proxy.ts',
'proxy.js',
'src/proxy.ts',
'src/proxy.js',
];
const wrongLevel = allPossible.find((p) => existsSync(join(projectDir, p)) && !correctPaths.includes(p));

const hasValidMiddleware = validPaths.some((p) => existsSync(join(projectDir, p)));
if (hasValidMiddleware) {
return; // Correctly placed
if (wrongLevel) {
const correctLevel = appInSrc ? 'src/' : 'project root';
issues.push({
type: 'file',
severity: 'error',
message: `${wrongLevel} is at the wrong level — app/ is in ${appInSrc ? 'src/' : 'root'}`,
hint: `Move it to ${expectedDir}${wrongLevel.replace(/^src\//, '')} (must be alongside app/ directory). Next.js silently ignores middleware/proxy files at the wrong level.`,
});
return;
}

// Check for misplaced middleware
const misplacedMiddleware = await fg(['**/middleware.{ts,js}'], {
// Check for deeply misplaced middleware
const misplaced = await fg(['**/{middleware,proxy}.{ts,js}'], {
cwd: projectDir,
ignore: ['node_modules/**'],
});

if (misplacedMiddleware.length > 0) {
if (misplaced.length > 0) {
issues.push({
type: 'file',
severity: 'error',
message: `middleware.ts found at wrong location: ${misplacedMiddleware[0]}`,
hint: 'Next.js middleware must be at project root (middleware.ts) or src/middleware.ts, not nested in app/ or other folders.',
message: `middleware/proxy found at wrong location: ${misplaced[0]}`,
hint: `Must be at ${expectedDir}middleware.ts or ${expectedDir}proxy.ts (alongside app/ directory).`,
});
}
}
Expand Down
15 changes: 8 additions & 7 deletions tests/evals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ Use `--no-fail` to run without exit code validation.

**Scenarios: 24 total (5 frameworks × 4-5 states)**

| State | Description |
| ------------------------ | ------------------------------- |
| `example` | Clean project, no existing auth |
| `example-auth0` | Project with Auth0 to migrate |
| `partial-install` | Half-completed AuthKit attempt |
| `typescript-strict` | Strict TypeScript configuration |
| `conflicting-middleware` | Existing middleware to merge |
| State | Description |
| ------------------------ | ------------------------------------------------------------------ |
| `example` | Clean project, no existing auth |
| `example-auth0` | Project with Auth0 to migrate |
| `partial-install` | Half-completed AuthKit attempt |
| `typescript-strict` | Strict TypeScript configuration |
| `conflicting-middleware` | Existing middleware to merge |
| `existing-middleware` | Next.js 16+ with existing middleware.ts (must not create proxy.ts) |

| Framework | Skill | Key Checks |
| ---------------- | ----------------------------- | ---------------------------------------------- |
Expand Down
1 change: 1 addition & 0 deletions tests/evals/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const STATES = [
'partial-install',
'typescript-strict',
'conflicting-middleware',
'existing-middleware',
'conflicting-auth',
];

Expand Down
49 changes: 41 additions & 8 deletions tests/evals/graders/nextjs.grader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,49 @@ export class NextjsGrader implements Grader {
);
checks.push(callbackCheck);

// Check middleware exists
checks.push(await this.fileGrader.checkFileExists('middleware.ts'));
// Check middleware or proxy exists at root or src/ (Next.js 16+ should use proxy.ts, 13-15 use middleware.ts)
const middlewareRoot = await this.fileGrader.checkFileExists('middleware.ts');
const middlewareSrc = await this.fileGrader.checkFileExists('src/middleware.ts');
const proxyRoot = await this.fileGrader.checkFileExists('proxy.ts');
const proxySrc = await this.fileGrader.checkFileExists('src/proxy.ts');

// Check middleware imports authkit SDK
const sdkImportChecks = await this.fileGrader.checkFileContains('middleware.ts', ['@workos-inc/authkit-nextjs']);
const middlewareExists = middlewareRoot.passed || middlewareSrc.passed;
const proxyExists = proxyRoot.passed || proxySrc.passed;

// Determine which file to check for authkit content
let middlewareFile: string;
if (proxyRoot.passed) middlewareFile = 'proxy.ts';
else if (proxySrc.passed) middlewareFile = 'src/proxy.ts';
else if (middlewareSrc.passed) middlewareFile = 'src/middleware.ts';
else middlewareFile = 'middleware.ts';

checks.push({
name: 'AuthKit middleware/proxy file exists',
passed: middlewareExists || proxyExists,
message: middlewareExists
? `middleware.ts exists${middlewareSrc.passed ? ' (src/)' : ''}`
: proxyExists
? `proxy.ts exists${proxySrc.passed ? ' (src/)' : ''}`
: 'Neither middleware.ts nor proxy.ts found',
});

// Next.js 16 throws error E900 if both middleware.ts and proxy.ts exist
if (middlewareExists && proxyExists) {
checks.push({
name: 'No middleware/proxy conflict',
passed: false,
message:
'Both middleware.ts and proxy.ts exist — Next.js 16 throws an error when both are present. Delete middleware.ts and use only proxy.ts.',
});
}

// Check middleware/proxy imports authkit SDK
const sdkImportChecks = await this.fileGrader.checkFileContains(middlewareFile, ['@workos-inc/authkit-nextjs']);
checks.push(...sdkImportChecks);

// Check for authkit integration: authkitMiddleware OR (authkit + handleAuthkitHeaders)
const middlewareChecks = await this.fileGrader.checkFileContains('middleware.ts', ['authkitMiddleware']);
const composableChecks = await this.fileGrader.checkFileContains('middleware.ts', [
const middlewareChecks = await this.fileGrader.checkFileContains(middlewareFile, ['authkitMiddleware']);
const composableChecks = await this.fileGrader.checkFileContains(middlewareFile, [
'authkit(',
'handleAuthkitHeaders',
]);
Expand All @@ -50,9 +83,9 @@ export class NextjsGrader implements Grader {
};
checks.push(authkitCheck);

// Check AuthKitProvider in layout or extracted providers file
// Check AuthKitProvider in layout or extracted providers file (app/ may be in src/)
const authKitProviderCheck = await this.fileGrader.checkFileWithPattern(
'app/**/*.tsx',
'{app,src/app}/**/*.tsx',
['AuthKitProvider'],
'AuthKitProvider in app',
);
Expand Down
3 changes: 2 additions & 1 deletion tests/evals/quality-key-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
*/
export const QUALITY_KEY_FILES: Record<string, string[]> = {
nextjs: [
// Middleware - auth protection layer
// Middleware - auth protection layer (proxy.ts for Next.js 16+ without existing middleware)
'middleware.ts',
'proxy.ts',
// Callback route - OAuth handling
'app/**/callback/**/route.ts',
'app/auth/callback/route.ts',
Expand Down
2 changes: 2 additions & 0 deletions tests/evals/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const STATE_LABELS: Record<string, string> = {
'partial-install': 'Partial',
'typescript-strict': 'Strict',
'conflicting-middleware': 'Conflict',
'existing-middleware': 'Existing MW',
'conflicting-auth': 'Conflict',
};

Expand All @@ -24,6 +25,7 @@ export function printMatrix(results: EvalResult[]): void {
'partial-install',
'typescript-strict',
'conflicting-middleware',
'existing-middleware',
'conflicting-auth',
];
states.sort((a, b) => stateOrder.indexOf(a) - stateOrder.indexOf(b));
Expand Down
3 changes: 2 additions & 1 deletion tests/evals/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ interface Scenario {
}

const SCENARIOS: Scenario[] = [
// Next.js (5 states)
// Next.js (6 states)
{ framework: 'nextjs', state: 'example', grader: NextjsGrader },
{ framework: 'nextjs', state: 'example-auth0', grader: NextjsGrader },
{ framework: 'nextjs', state: 'partial-install', grader: NextjsGrader },
{ framework: 'nextjs', state: 'typescript-strict', grader: NextjsGrader },
{ framework: 'nextjs', state: 'conflicting-middleware', grader: NextjsGrader },
{ framework: 'nextjs', state: 'existing-middleware', grader: NextjsGrader },

// React SPA (5 states)
{ framework: 'react', state: 'example', grader: ReactGrader },
Expand Down
26 changes: 26 additions & 0 deletions tests/fixtures/nextjs/existing-middleware/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Next.js 16 - Existing Middleware in src/ Fixture

## Edge Case Description

This fixture is a Next.js 16+ project using `src/` directory structure that already has a `src/middleware.ts` with a no-op passthrough and empty matcher. Since `middleware.ts` is deprecated in Next.js 16 (replaced by `proxy.ts`), and Next.js 16 throws error E900 if both files exist, the agent must delete the existing `src/middleware.ts` and create `src/proxy.ts` with the AuthKit config.

The `src/` directory placement is critical — the middleware/proxy file must be placed alongside the `app/` directory, not at the project root.

## Expected Agent Behavior

- Detect `src/app/` directory structure
- Detect existing `src/middleware.ts`
- Delete `src/middleware.ts` (deprecated in Next.js 16)
- Create `src/proxy.ts` with AuthKit middleware config (not at project root)
- Never leave both `middleware.ts` and `proxy.ts` in place

## Files of Interest

- `src/middleware.ts` - Deprecated no-op middleware that must be replaced with `src/proxy.ts`

## Success Criteria

- [ ] `src/proxy.ts` is created with AuthKit middleware
- [ ] `src/middleware.ts` is deleted
- [ ] Matcher covers application routes
- [ ] Build succeeds
4 changes: 4 additions & 0 deletions tests/fixtures/nextjs/existing-middleware/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};

export default nextConfig;
21 changes: 21 additions & 0 deletions tests/fixtures/nextjs/existing-middleware/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "nextjs-existing-middleware-fixture",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "^16.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.4.0"
}
}
Loading