Skip to content

feat(auth): pluggable authentication providers system - #892

Merged
murdore merged 1 commit into
releasefrom
feat/authentication-providers
Mar 25, 2026
Merged

murdore merged 1 commit into
releasefrom
feat/authentication-providers

Conversation

@murdore

@murdore murdore commented Mar 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Adds a pluggable authentication layer following the existing Factory + Registry pattern with 12 providers: Auth0, Clerk, Firebase, Supabase, AWS Cognito, Keycloak, BetterAuth, WorkOS, JWT (HMAC/RSA/ECDSA), OAuth2/OIDC, Custom, and a BaseAuthProvider
  • Implements AuthMiddleware (token extraction from header/cookie/query, RBAC enforcement, public route bypass) and per-user rate limiting middleware (token bucket algorithm, memory + Redis backends)
  • Integrates into SDK (NeuroLink constructor auth config, generate()/stream() requestContext and auth.token support) and CLI (auth providers/validate/health/login/logout/status/refresh commands)
  • Includes SessionManager with memory and Redis storage, AsyncLocalStorage-based auth context propagation, 13 typed error classes, and 22 test cases across 4 sections

What's included

Core System (~3,200 lines)

  • AuthProviderFactory — Singleton factory with lazy dynamic imports for all 12 providers
  • AuthProviderRegistry — Metadata tracking, feature discovery, health checks with latency metrics
  • BaseAuthProvider — Abstract base class with token extraction, permission hierarchy (tools:* includes tools:execute), role hierarchy, event emitter
  • authContext — AsyncLocalStorage implicit context propagation + AuthContextHolder fallback
  • authErrors — 13 error classes, 20 error codes, type guards (isAuthError, isTokenError, etc.)
  • sessionManager — MemorySessionStorage, RedisSessionStorage, SessionManager with auto-refresh
  • RequestContext — Type-safe request-scoped Map with reserved keys

Auth Providers (~5,300 lines)

Provider Key Features
Auth0 JWKS validation, Management API, custom claims namespace
Clerk JWT validation, Clerk-specific JWKS
Firebase Google public keys + REST API fallback
Supabase JWT validation, service role key support
AWS Cognito User Pools, JWKS, MFA support
Keycloak OpenID Connect, JWKS, LDAP/SSO
BetterAuth Self-hosted, social providers (GitHub, Google, Discord)
WorkOS Enterprise SSO, directory sync
JWT Generic HMAC/RSA/ECDSA via jose library
OAuth2/OIDC Authorization/token/userinfo endpoints, PKCE
Custom User-supplied validateToken function

Middleware (~900 lines)

  • AuthMiddleware — Token extraction (header/cookie/query/custom), RBAC enforcement, public route bypass, optional auth mode
  • rateLimitByUser — Token bucket algorithm, per-role/per-user limits, memory + Redis storage

SDK Integration

  • NeurolinkConstructorConfig.auth accepts pre-built provider or { type, config } for factory creation
  • generate()/stream() accept requestContext (pre-validated user context) and auth.token (runtime token validation)
  • setAuthProvider() / getAuthProvider() methods on NeuroLink instance
  • Server routes propagate user/session context to generate/stream calls

CLI Integration

  • auth providers — List available providers with metadata
  • auth validate <token> — Validate token against configured provider
  • auth health — Check provider health status
  • auth login/logout/status/refresh — Auth lifecycle commands

Type System (878 lines)

  • AuthProviderType, AuthUser, AuthSession, TokenValidationResult, AuthorizationResult
  • Provider-specific config types for all 12 providers
  • AuthEventType / AuthEventData for lifecycle events
  • 20 typed AuthErrorCode values

Test plan

  • Section 1: Server Auth — Protected Endpoints (8 tests): No token → 401, valid token → 200, invalid token → 401, public routes skip auth, user context propagation, Bearer prefix handling, backward compatibility
  • Section 2: Server Auth — RBAC (5 tests): Admin wildcard access, user permission matching, viewer denied, wildcard across endpoints, error message includes missing permission
  • Section 3: Per-Call Auth — Pre-validated Context (5 tests): generate()/stream() accept requestContext with userId, roles, empty context
  • Section 4: Per-Call Auth — Token Validation (4 tests): Provider token validation flow, invalid token rejection, no provider returns undefined, streaming with auth token
  • TypeScript compilation passes with tsc --noEmit --strict
  • ESLint passes (0 errors, warnings only)

Summary by CodeRabbit

  • New Features

    • Multi-provider authentication (11 providers) with per-call token validation for generate/stream
    • Session management (memory/Redis) and per-user rate limiting
    • Authentication middleware with RBAC, protected routes, and Express integration
    • Runtime APIs to set/get auth provider and manage per-request auth context
    • CLI: list providers, validate tokens, and check provider health
  • Documentation

    • Added authentication architecture and provider setup guides

Copilot AI review requested due to automatic review settings March 21, 2026 10:28
@vercel

vercel Bot commented Mar 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
neurolink Ready Ready Preview, Comment Mar 25, 2026 11:02pm

@github-actions

github-actions Bot commented Mar 21, 2026 •

Copy link
Copy Markdown
Contributor

✅ Single Commit Policy - COMPLIANT

Status: Policy requirements met • 1 commit • Valid format • Ready for merge

📊 View validation details

📝 Commit Details

  • Hash: 0013d914b0756a31af307aa13f4b8925e4232c45
  • Message: feat(auth): add authentication providers system with 12 providers, middleware, RBAC, and session management
  • Author: Sachin Sharma

✅ Validation Results

  • Single commit requirement met
  • No merge commits in branch
  • Semantic commit message format verified
  • Ready for squash merge to release branch

🤖 Automated validation by NeuroLink Single Commit Enforcement

@coderabbitai

coderabbitai Bot commented Mar 21, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bbc8c9cd-6be4-48aa-b1e2-8b064455a8d1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a pluggable multi-provider authentication subsystem: provider factory & registry, 11 provider implementations, base provider/session primitives, middleware (auth, RBAC, rate-limiting), session manager (memory/Redis), CLI commands, SDK/server integration (per-call token validation and context merging), types, docs, and tests.

Changes

Cohort / File(s) Summary
Package
package.json
Adds jose dependency (^6.1.3) for JWT/JWKS operations.
CLI
src/cli/commands/authProviders.ts, src/cli/factories/authCommandFactory.ts, src/cli/parser.ts
New auth subcommands: providers, validate <token>, health; CLI option wiring and dynamic handler imports.
Factory & Registry
src/lib/auth/AuthProviderFactory.ts, src/lib/auth/AuthProviderRegistry.ts
New singleton factory and registry for provider registration, aliasing, metadata discovery, lazy dynamic imports, provider creation, and health checks.
Core Types & Re-exports
src/lib/types/authTypes.ts, src/lib/types/*.ts, src/lib/auth/index.ts, src/lib/index.ts
Comprehensive auth type surface, new NeuroLink auth config types, and centralized re-exports for the auth subsystem.
Base Provider & Primitives
src/lib/auth/authProvider.ts, src/lib/auth/providers/BaseAuthProvider.ts, src/lib/auth/RequestContext.ts
Abstract BaseAuthProvider, RequestContext, session helpers, in-memory session storage, RBAC/authorization helpers and token extraction defaults.
Concrete Providers
src/lib/auth/providers/*
src/lib/auth/providers/auth0.ts, clerk.ts, firebase.ts, supabase.ts, cognito.ts, keycloak.ts, betterAuth.ts, workos.ts, oauth2.ts, jwt.ts, custom.ts
Adds 11 provider implementations (Auth0, Clerk, Firebase, Supabase, Cognito, Keycloak, Better Auth, WorkOS, OAuth2, JWT, Custom) with token validation, user mapping, sessions, health, and optional provider-specific APIs.
Middleware & Rate Limiting
src/lib/auth/middleware/AuthMiddleware.ts, src/lib/auth/middleware/rateLimitByUser.ts
Auth middleware (token extraction, createAuthMiddleware, RBAC, protected composition, Express adapter) and per-user token-bucket rate limiting with memory/Redis storage and atomic Redis script fallback.
Session Management
src/lib/auth/sessionManager.ts
SessionManager abstraction with Memory/Redis storage backends, storage factory, session lifecycle (create/get/refresh/destroy), health and clear.
Auth Context & Errors
src/lib/auth/authContext.ts, src/lib/auth/authErrors.ts
AsyncLocalStorage-based request auth context with global fallback, role/permission helpers, and a rich auth error class hierarchy with type guards.
Server/SDK Integration
src/lib/neurolink.ts, src/lib/server/routes/agentRoutes.ts, src/lib/auth/serverBridge.ts, src/lib/mcp/toolRegistry.ts
Per-call auth token handling in generate/stream (validated identity merged into request context), hardened agent routes to prefer authenticated ctx, server bridge helper, and tool execution userId fallback to auth context.
Registry API Changes
src/lib/core/infrastructure/baseRegistry.ts, src/lib/rag/...
BaseRegistry.register signature changed to accept aliases + optional metadata; calling registries updated to pass aliases and metadata options.
Docs & Sidebars
docs/features/authentication-providers.md, docs/advanced/auth-architecture.md, docs/features/index.md, docs-site/sidebars.ts
New feature and architecture docs, sidebar and index updates describing auth subsystem and usage.
Tests & Fixtures
test/continuous-test-suite-auth.ts, test/fixtures/auth/*
Adds an end-to-end auth regression runner and fixtures for provider configs, sessions, and credentials.
Misc
src/lib/core/infrastructure/baseRegistry.ts, src/lib/rag/*, src/lib/mcp/toolRegistry.ts
Adjustments to registry aliasing and a small toolRegistry change to use auth context fallback for exec userId.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant NeuroLink as NeuroLink
  participant Factory as AuthProviderFactory
  participant Provider as AuthProvider
  participant Session as SessionStorage

  Client->>NeuroLink: generate/stream(auth: { token })
  NeuroLink->>Factory: ensure/create provider (lazy)
  Factory->>Provider: instantiate(provider-config)
  NeuroLink->>Provider: authenticateToken(token) (with timeout)
  Provider->>Provider: verify JWT/JWKS or call provider API
  alt valid & user returned
    Provider->>Session: get/create session for user
    Session-->>Provider: AuthSession
    Provider-->>NeuroLink: TokenValidationResult (user, expiresAt)
    NeuroLink-->>Client: proceed with merged context (userId, userEmail, userRoles)
  else invalid or error
    Provider-->>NeuroLink: invalid / error
    NeuroLink-->>Client: throw AuthenticationFailedError / InvalidTokenError
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

released

Suggested reviewers

  • charan-vadd

Poem

🐰 I hopped through code to stitch a secure band,
Providers lined up, eleven across the land,
Tokens checked, sessions snug in store,
RBAC guards the carrots — no spoofing at the door,
Happy hops — NeuroLink now guards every hand!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/authentication-providers

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@github-actions

github-actions Bot commented Mar 21, 2026 •

Copy link
Copy Markdown
Contributor

Documentation Validation Results

🚀 Documentation validation passed!

Check Status Result
Frontmatter Validation ✅ Passed
TypeScript Check ✅ Passed
Build ✅ Passed
Link Validation ✅ Passed

📦 Build artifact uploaded successfully. Ready for deployment preview.

Commit: 138fb06c519d46a1b89394cd59edad55abca44ec | Workflow: View logs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a pluggable, multi-provider authentication system and wires it into the NeuroLink SDK and CLI, enabling per-call token validation or pre-validated request context to flow into generate/stream and downstream tool execution.

Changes:

  • Adds auth provider factory/registry, provider implementations, session manager, auth context utilities, and error types.
  • Extends generate()/stream() option types and NeuroLink runtime to accept requestContext and auth: { token }.
  • Adds CLI subcommands for provider listing, token validation, and health checks plus supporting fixtures/spec docs.

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
test/fixtures/auth/test-credentials.json Adds placeholder test users/tokens/JWKS fixture data
test/fixtures/auth/session-data.json Adds session and rate-limit fixture data
test/fixtures/auth/provider-config.json Adds provider config fixtures and RBAC/middleware/session configs
src/lib/types/streamTypes.ts Adds requestContext and auth.token to StreamOptions
src/lib/types/generateTypes.ts Adds requestContext and auth.token to GenerateOptions
src/lib/types/index.ts Re-exports new auth-related types from types/authTypes
src/lib/types/configTypes.ts Adds NeurolinkConstructorConfig.auth / NeuroLinkAuthConfig
src/lib/types/authTypes.ts Introduces a new auth type system (provider/user/session/context contracts)
src/lib/server/routes/agentRoutes.ts Propagates server auth context fields into generate/stream context
src/lib/neurolink.ts Lazy auth provider init; per-call token validation; requestContext merging; auth context getters/setters
src/lib/mcp/toolRegistry.ts Pulls userId from async-local auth context when absent from exec context
src/lib/index.ts Exports auth factory/registry/providers/middleware/session/context/errors/types at top-level
src/lib/auth/sessionManager.ts Adds memory + Redis session storage and SessionManager
src/lib/auth/serverBridge.ts Adds adapter to use provider authenticateToken() with existing server middleware validate callback shape
src/lib/auth/providers/auth0.ts Auth0 provider implementation
src/lib/auth/providers/clerk.ts Clerk provider implementation
src/lib/auth/providers/firebase.ts Firebase provider implementation
src/lib/auth/providers/supabase.ts Supabase provider implementation
src/lib/auth/providers/workos.ts WorkOS provider implementation
src/lib/auth/providers/betterAuth.ts Better Auth provider implementation
src/lib/auth/providers/oauth2.ts Generic OAuth2/OIDC provider implementation
src/lib/auth/providers/jwt.ts Generic JWT provider implementation
src/lib/auth/providers/custom.ts Custom provider implementation
src/lib/auth/providers/CognitoProvider.ts Cognito provider implementation (provider-specific logic)
src/lib/auth/index.ts Aggregates/auth-exports multi-provider auth system from src/lib/auth/*
src/lib/auth/authProvider.ts Adds new BaseAuthProvider (token extraction, authz helpers, request auth flow)
src/lib/auth/authErrors.ts Adds auth error classes + type guards
src/lib/auth/authContext.ts Adds AsyncLocalStorage-based auth context utilities + fallback holder
src/lib/auth/RequestContext.ts Adds request-scoped context map with reserved-key protection
src/lib/auth/AuthProviderRegistry.ts Adds provider registry + discovery + (currently shallow) health checks
src/lib/auth/AuthProviderFactory.ts Adds singleton factory w/ lazy dynamic imports for providers
src/cli/parser.ts Registers auth command group in CLI
src/cli/factories/authCommandFactory.ts Adds auth providers/validate/health subcommands
src/cli/commands/authProviders.ts Implements provider listing/token validation/health check handlers
package.json Adds jose dependency for JWT/JWKS validation
docs/superpowers/specs/2026-03-16-auth-providers-redesign.md Adds design spec documenting intended auth integration contract

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib/auth/AuthProviderRegistry.ts Outdated
AuthProviderConfig,
AuthProviderType,
MastraAuthProvider,
} from "./types/authTypes.js";

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AuthProviderRegistry imports AuthProviderConfig/AuthProviderType/MastraAuthProvider from ./types/authTypes.js, but AuthProviderFactory (used by the registry) is typed against ../types/authTypes.js. This mixes two different auth type definitions and forces as any casts (see createProvider). Recommend standardizing both registry and factory on the same authTypes module to avoid subtle incompatibilities and TS export conflicts.

Suggested change
} from "./types/authTypes.js";
} from "../types/authTypes.js";

Copilot uses AI. Check for mistakes.
Comment on lines +260 to +270
/**
* Auth provider choices for multi-provider commands
*/
private static readonly AUTH_PROVIDER_CHOICES = [
"auth0",
"clerk",
"supabase",
"firebase",
"workos",
"better-auth",
] as const;

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AUTH_PROVIDER_CHOICES limits auth validate/auth health to a subset of providers, but auth providers lists additional ones (e.g., jwt, oauth2, cognito, keycloak). This makes the CLI inconsistent and prevents validating/health-checking providers that the system claims to support. Consider either expanding the choices to all supported providers or filtering the list output to only those supported by validate/health.

Copilot uses AI. Check for mistakes.
Comment thread src/lib/neurolink.ts
Comment on lines +2995 to +3031
// Handle per-call auth token validation
if (options.auth?.token) {
await this.ensureAuthProvider();
if (!this.authProvider) {
throw new Error(
"No auth provider configured. Set auth in constructor or via setAuthProvider() before using auth: { token }.",
);
}
const authResult = await this.authProvider.authenticateToken(
options.auth.token,
);
if (!authResult.valid) {
const { InvalidTokenError } = await import(
"./auth/authErrors.js"
);
throw new InvalidTokenError(
authResult.error || "Token validation failed",
this.authProvider.type,
);
}
// Merge validated user into context
if (authResult.user) {
options.context = {
...((options.context as Record<string, unknown>) || {}),
userId: authResult.user.id,
userEmail: authResult.user.email,
userRoles: authResult.user.roles,
};
}
}

// Handle pre-validated requestContext
if (options.requestContext) {
options.context = {
...((options.context as Record<string, unknown>) || {}),
...options.requestContext,
};

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate() merges options.requestContext into options.context after token validation, so a caller can provide both auth: { token } and a conflicting requestContext to override userId/userRoles derived from the validated token. This undermines the point of token validation and can enable privilege escalation. Consider either (a) rejecting calls that provide both auth.token and requestContext, or (b) giving token-derived fields precedence (merge requestContext first, then overlay validated user fields, or explicitly omit/lock identity keys when merging).

Copilot uses AI. Check for mistakes.
Comment thread src/lib/neurolink.ts Outdated
Comment on lines +10901 to +10906
async setAuthProvider(config: NeuroLinkAuthConfig): Promise<void> {
if ("provider" in config) {
this.authProvider = config.provider;
logger.info(`Auth provider set: ${config.provider.type}`);
} else {
const { AuthProviderFactory } = await import(

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This setAuthProvider implementation only supports the wrapper shape ({ provider } or { type, config }). If the intended public API includes passing a provider instance directly (as described in the spec), calls like setAuthProvider(new Auth0Provider(...)) will not work. Either widen the accepted parameter type / runtime branching to handle MastraAuthProvider directly, or adjust the documented contract to require the wrapper.

Copilot uses AI. Check for mistakes.
Comment thread src/lib/auth/authProvider.ts Outdated
Comment on lines +78 to +91
if (strategy?.fromHeader) {
const headerName = strategy.fromHeader.name.toLowerCase();
const headerValue = context.headers[headerName];

if (typeof headerValue === "string") {
if (strategy.fromHeader.scheme) {
const prefix = `${strategy.fromHeader.scheme} `;
if (headerValue.startsWith(prefix)) {
return headerValue.slice(prefix.length);
}
} else {
return headerValue;
}
}

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseAuthProvider.extractToken() lowercases the configured header name and then does a direct lookup (context.headers[headerName]). If callers populate headers with canonical casing (e.g., Authorization) this will fail to find the token. Consider doing a case-insensitive header lookup (iterate entries like AuthMiddleware.extractToken does) or normalizing headers when building AuthRequestContext.

Copilot uses AI. Check for mistakes.
Comment on lines +403 to +477
function buildProviderConfig(
argv: ArgumentsCamelCase<AuthValidateArgs | AuthHealthArgs>,
): Record<string, unknown> | null {
switch (argv.provider) {
case "auth0": {
const domain = argv.domain || process.env.AUTH0_DOMAIN;
const clientId = argv.clientId || process.env.AUTH0_CLIENT_ID;
if (!domain || !clientId) {
return null;
}
return {
domain,
clientId,
audience: process.env.AUTH0_AUDIENCE,
};
}

case "clerk": {
const secretKey = argv.secretKey || process.env.CLERK_SECRET_KEY;
const publishableKey = process.env.CLERK_PUBLISHABLE_KEY || "";
if (!secretKey) {
return null;
}
return {
publishableKey,
secretKey,
};
}

case "supabase": {
const url = argv.url || process.env.SUPABASE_URL;
const anonKey = argv.anonKey || process.env.SUPABASE_ANON_KEY;
if (!url || !anonKey) {
return null;
}
return {
url,
anonKey,
jwtSecret: process.env.SUPABASE_JWT_SECRET,
};
}

case "firebase": {
const projectId = process.env.FIREBASE_PROJECT_ID;
if (!projectId) {
return null;
}
return {
projectId,
apiKey: process.env.FIREBASE_API_KEY,
};
}

case "workos": {
const apiKey = argv.apiKey || process.env.WORKOS_API_KEY;
const clientId = argv.clientId || process.env.WORKOS_CLIENT_ID;
if (!apiKey || !clientId) {
return null;
}
return {
apiKey,
clientId,
};
}

case "better-auth": {
const secret = process.env.BETTER_AUTH_SECRET;
const baseUrl = argv.url || process.env.BETTER_AUTH_BASE_URL;
if (!secret || !baseUrl) {
return null;
}
return {
secret,
baseUrl,
};

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildProviderConfig() returns provider config objects without a type field, even though AuthProviderConfig (and most provider constructors) are modeled as { type, ... }. Right now this is masked by as AuthProviderConfig, but it weakens type safety and can break providers that rely on config.type. Consider adding type: argv.provider to the returned config for each case (and validating required fields consistently with PROVIDER_INFO).

Copilot uses AI. Check for mistakes.
Comment thread src/lib/auth/sessionManager.ts Outdated
Comment on lines +155 to +157
// Fall back to in-memory if Redis is not available
logger.warn(
"Redis not available, falling back to in-memory session storage",

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RedisSessionStorage.getClient() logs that it is "falling back to in-memory session storage" when ioredis import fails, but it then throws an error and does not actually fall back. This is misleading and can cause unexpected runtime failures in environments without ioredis. Either implement a real fallback to MemorySessionStorage at the SessionManager level, or update the log/message to reflect that Redis is required for storage: "redis".

Suggested change
// Fall back to in-memory if Redis is not available
logger.warn(
"Redis not available, falling back to in-memory session storage",
logger.error(
"Redis client (ioredis) not available. When using storage: \"redis\", the ioredis package must be installed and Redis must be reachable.",

Copilot uses AI. Check for mistakes.
Comment thread src/lib/neurolink.ts
Comment on lines +5558 to +5595
// Handle per-call auth token validation
if (options.auth?.token) {
await this.ensureAuthProvider();
if (!this.authProvider) {
throw new Error(
"No auth provider configured. Set auth in constructor or via setAuthProvider() before using auth: { token }.",
);
}
const authResult = await this.authProvider.authenticateToken(
options.auth.token,
);
if (!authResult.valid) {
const { InvalidTokenError } = await import(
"./auth/authErrors.js"
);
throw new InvalidTokenError(
authResult.error || "Token validation failed",
this.authProvider.type,
);
}
// Merge validated user into context
if (authResult.user) {
options.context = {
...((options.context as Record<string, unknown>) || {}),
userId: authResult.user.id,
userEmail: authResult.user.email,
userRoles: authResult.user.roles,
};
}
}

// Handle pre-validated requestContext
if (options.requestContext) {
options.context = {
...((options.context as Record<string, unknown>) || {}),
...options.requestContext,
};
}

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue in stream(): options.requestContext is merged after token validation, allowing a caller to override userId/userRoles derived from auth.token. For security, reject using both, or ensure token-derived identity fields take precedence / cannot be overridden by requestContext.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +118
/**
* Authentication configuration for NeuroLink SDK
*/
export type NeuroLinkAuthConfig =
| { provider: MastraAuthProvider }
| { type: "auth0"; config: Auth0Config }
| { type: "clerk"; config: ClerkConfig }
| { type: "firebase"; config: FirebaseConfig }
| { type: "supabase"; config: SupabaseConfig }
| { type: "workos"; config: WorkOSConfig }
| { type: "better-auth"; config: BetterAuthConfig }
| { type: AuthProviderType; config: AuthProviderConfig };

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NeuroLinkAuthConfig only allows passing a pre-built provider via { provider: MastraAuthProvider }, but the PR description/spec show auth: new Auth0Provider(...) (direct instance) as supported. If a user follows the documented API, it won't type-check, and NeuroLink's runtime logic also expects the wrapper shape. Consider allowing auth?: MastraAuthProvider | { provider: MastraAuthProvider } | { type, config } (or updating docs/constructor handling to match the intended contract).

Copilot uses AI. Check for mistakes.
Comment thread src/lib/index.ts Outdated
Comment on lines +1308 to +1320
// Auth Types (re-exported for convenience)
export type {
AuthProviderType,
AuthProviderConfig,
AuthUser,
AuthSession,
AuthenticatedContext,
AuthMiddlewareConfig,
TokenClaims,
SessionConfig,
RBACConfig,
MastraAuthProvider,
} from "./auth/types/authTypes.js";

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/lib/index.ts already does export * from "./types/index.js";, and types/index.ts now re-exports AuthProviderType, AuthProviderConfig, etc. Re-exporting the same names again here from ./auth/types/authTypes.js will cause duplicate/conflicting exports (and also mixes two different auth type systems). Consider removing this block, or re-exporting the auth types from a single canonical module to avoid name collisions and inconsistent public types.

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/cli/factories/authCommandFactory.ts (1)

15-22: ⚠️ Potential issue | 🟠 Major

Remove as any casts and use properly typed args from authProviders.ts.

The "providers" command accepts format: "table" (line 93) but AuthCommandArgs.format only allows "text" | "json". The as any casts with eslint-disable-next-line @typescript-eslint/no-explicit-any`` (lines 103, 115, 127) hide this type mismatch and violate the enforced no-explicit-any rule for src/.

Import AuthProvidersArgs, AuthValidateArgs, and AuthHealthArgs from authProviders.ts and use them directly instead of casting to any. The types already exist and properly account for all command options—AuthProvidersArgs includes "table" in the format union while validate/health use the narrower "text" | "json" set.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/factories/authCommandFactory.ts` around lines 15 - 22, The
AuthCommandArgs type is too broad and the file uses multiple "as any" casts to
hide mismatched formats; import the specific types AuthProvidersArgs,
AuthValidateArgs, and AuthHealthArgs from authProviders.ts and replace uses of
AuthCommandArgs (and remove the eslint-disable-next-line casts) with the correct
provider-specific types for the corresponding commands (e.g., use
AuthProvidersArgs for the "providers" command so "table" is allowed, and
AuthValidateArgs/AuthHealthArgs for validate/health commands to keep
"text"|"json"); update the function/method signatures and argument typings
(e.g., where AuthCommandArgs is referenced and where the as any casts occur) to
use these imported types and remove all explicit any casts.
src/lib/auth/index.ts (1)

54-58: ⚠️ Potential issue | 🟡 Minor

Potential TokenValidationResult type collision.

Line 54 exports TokenValidationResult from "./anthropicOAuth.js", and line 265 exports the same name (aliased as AuthTokenValidationResult) from "./types/authTypes.js". While the alias avoids direct collision, consumers importing TokenValidationResult will get the OAuth version, which may cause confusion.

💡 Consider consistent aliasing
 // OAuth types (canonical definitions in types/subscriptionTypes.ts)
 export type {
   OAuthTokenResponse,
   OAuthFlowTokens,
   OAuthFlowTokens as OAuthTokens,
-  TokenValidationResult,
+  TokenValidationResult as OAuthTokenValidationResult,
   AnthropicOAuthConfig,
   PKCEParams,
   CallbackResult,
 } from "./anthropicOAuth.js";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/index.ts` around lines 54 - 58, The export TokenValidationResult
from "./anthropicOAuth.js" conflicts by name with the other exported type
aliased as AuthTokenValidationResult from "./types/authTypes.js", so update the
re-exports to use distinct, explicit aliases: change the export from
"./anthropicOAuth.js" (TokenValidationResult) to a clear alias like
OAuthTokenValidationResult (or AnthropicTokenValidationResult) and ensure the
other type remains AuthTokenValidationResult; then update any internal imports
that rely on the unaliased name to use the new alias (look for references to
TokenValidationResult, AuthTokenValidationResult, and the file
"./anthropicOAuth.js"/"./types/authTypes.js" to apply the rename).
🟠 Major comments (23)
src/lib/auth/middleware/rateLimitByUser.ts-487-500 (1)

487-500: ⚠️ Potential issue | 🟠 Major

Type mismatch: authMiddleware parameter expects Request but createAuthMiddleware returns a handler for AuthRequestContext.

Per the context snippet from AuthMiddleware.ts (lines 192-215), createAuthMiddleware returns MiddlewareHandler<AuthRequestContext>, which is a function accepting AuthRequestContext, not a raw Request object. The signature here expects (request: Request) => Promise<...>, which won't match.

Either:

  1. Update this signature to accept (context: AuthRequestContext) => Promise<...>, or
  2. Provide an adapter that converts Request to AuthRequestContext before calling the auth middleware.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/middleware/rateLimitByUser.ts` around lines 487 - 500, The auth
middleware parameter type is incorrect: createAuthMiddleware returns a
MiddlewareHandler<AuthRequestContext> (a function accepting AuthRequestContext)
but createAuthenticatedRateLimitMiddleware currently types authMiddleware as
(request: Request) => Promise<...>. Fix by changing the authMiddleware parameter
type to MiddlewareHandler<AuthRequestContext> (or the equivalent type alias used
in your codebase) so signatures match, and update internal calls to pass an
AuthRequestContext; alternatively implement a small adapter inside
createAuthenticatedRateLimitMiddleware that constructs an AuthRequestContext
from the incoming Request and then calls the provided auth middleware (reference
symbols: createAuthenticatedRateLimitMiddleware, createAuthMiddleware,
MiddlewareHandler<AuthRequestContext>, AuthRequestContext, authMiddleware).
src/lib/types/index.ts-255-296 (1)

255-296: ⚠️ Potential issue | 🟠 Major

Add MastraAuthProvider to the selective auth type exports.

The core auth provider interface is missing from the re-exports. While MastraAuthProvider is available via the main src/lib/index.ts entry point, it's excluded from the src/lib/types/index.ts auth exports, creating an inconsistency with how the provider domain handles its core interface (AIProvider). All implementations of the auth system extend MastraAuthProvider, and external consumers implementing custom auth providers will need this type exported here for consistency.

Add to lines 256-296:

MastraAuthProvider,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/types/index.ts` around lines 255 - 296, The export block in
src/lib/types/index.ts omits the core provider interface MastraAuthProvider,
causing consumers implementing custom auth providers to lack the type; add
MastraAuthProvider to the selective re-exports in the auth types export list
(the same block that exports AuthProviderType, AuthProviderConfig, AuthUser,
AuthSession, etc.) so MastraAuthProvider is re-exported alongside those other
auth types.
test/fixtures/auth/test-credentials.json-46-107 (1)

46-107: ⚠️ Potential issue | 🟠 Major

These "valid" token fixtures are already expired and still trip secret scanning.

validToken, noRolesToken, and adminToken all expire on February 1, 2024, so any test that checks exp against the wall clock will now treat them as invalid. The committed JWT-shaped strings also trigger Betterleaks on Lines 63, 75, 91, and 106. Generate these tokens relative to Date.now() in the test harness, or keep only the payload metadata here and use obvious non-token placeholders.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/fixtures/auth/test-credentials.json` around lines 46 - 107, The fixtures
validToken, noRolesToken, and adminToken contain hard-coded JWT strings that are
expired and trigger secret scanning; remove the baked token strings for those
entries (token fields on validToken, noRolesToken, adminToken) or replace them
with obvious placeholders like "GENERATE_AT_RUNTIME", keep the payload metadata
as-is, and update tests to generate real JWTs at runtime (use Date.now()/new
Date() to set iat/exp relative to current time) in the test harness function
that consumes these fixtures (refer to the fixture keys validToken,
noRolesToken, adminToken and the test helper that builds tokens) so tests
validate expiration correctly and avoid committing secrets.
test/fixtures/auth/provider-config.json-6-125 (1)

6-125: ⚠️ Potential issue | 🟠 Major

Use non-secret-shaped placeholders in this fixture.

Values like AIza..., sk_test_..., and the JWT-looking Supabase keys are fake, but secret scanners still treat them as credentials—Betterleaks already flags Line 54. Keeping credential-shaped strings in git creates noisy CI and desensitizes the repo to real leaks. Prefer obvious sentinels such as <SUPABASE_ANON_KEY_PLACEHOLDER> or load realistic samples from ignored env files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/fixtures/auth/provider-config.json` around lines 6 - 125, Replace any
credential-shaped literal values in the provider fixtures (e.g., the firebase
apiKey, supabase anonKey/serviceRoleKey/jwtSecret, workos apiKey, clerk
secretKey, auth0 clientSecret, better-auth secret, jwt secret, etc.) with
clearly non-secret placeholders like <FIREBASE_API_KEY_PLACEHOLDER>,
<SUPABASE_ANON_KEY_PLACEHOLDER>, <WORKOS_API_KEY_PLACEHOLDER> or load them from
a test-only env loader; update entries under "firebase", "supabase", "workos",
"clerk", "auth0", "better-auth", and "jwt" to use those sentinel strings so
secret scanners no longer flag them while preserving the shape of the fixture
for tests.
src/lib/auth/serverBridge.ts-18-27 (1)

18-27: ⚠️ Potential issue | 🟠 Major

Handle valid tokens that only return claims.

TokenValidationResult.user is optional, and the server middleware treats null as an invalid token. Returning null whenever result.user is missing will reject providers that validate successfully but only surface claims. Fall back to result.claims before failing this bridge.

Suggested fallback
   return async (token: string) => {
     const result = await provider.authenticateToken(token);
-    if (!result.valid || !result.user) {
+    if (!result.valid) {
       return null;
     }
+    const id =
+      result.user?.id ??
+      (typeof result.claims?.sub === "string" ? result.claims.sub : undefined) ??
+      (typeof result.claims?.user_id === "string"
+        ? result.claims.user_id
+        : undefined);
+    if (!id) {
+      return null;
+    }
     return {
-      id: result.user.id,
-      email: result.user.email,
-      roles: result.user.roles,
+      id,
+      email:
+        result.user?.email ??
+        (typeof result.claims?.email === "string"
+          ? result.claims.email
+          : undefined),
+      roles:
+        result.user?.roles ??
+        (Array.isArray(result.claims?.roles)
+          ? result.claims.roles.filter(
+              (role): role is string => typeof role === "string",
+            )
+          : undefined),
     };
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/serverBridge.ts` around lines 18 - 27, The bridge currently
returns null when provider.authenticateToken yields a valid result with no
result.user; instead, update the returned async function (the token bridge that
calls provider.authenticateToken) to fall back to result.claims before returning
null: if result.valid but result.user is missing, build the user object from
result.claims (e.g. use claims.sub or claims.id for id, claims.email for email,
and claims.roles for roles) and return that constructed user shape; only return
null when result.valid is false and there are no usable claims.
test/fixtures/auth/session-data.json-6-128 (1)

6-128: ⚠️ Potential issue | 🟠 Major

All sessions marked isValid: true are already expired.

validSession, adminSession, managerSession, aboutToExpireSession, and both multiDevice entries expire between January 31, 2026 and February 1, 2026. Any TTL-aware test will now reject them despite isValid: true. Make these timestamps relative in the test setup, or move them far enough into the future to avoid time-bombed fixtures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/fixtures/auth/session-data.json` around lines 6 - 128, The fixture marks
several sessions (validSession, adminSession, managerSession,
aboutToExpireSession, multiDeviceSession1, multiDeviceSession2) as isValid: true
but uses hardcoded expiresAt timestamps that are already expired; update these
session objects to use future or relative timestamps (e.g., compute
expiresAt/createdAt/lastActivityAt relative to now in the test setup or set them
far enough in the future) and keep aboutToExpireSession as a relative
short-lived expiry if the test needs it; ensure refreshToken/accessToken fields
remain unchanged and only timestamp fields are adjusted so TTL-aware tests
accept the sessions.
src/cli/factories/authCommandFactory.ts-263-318 (1)

263-318: ⚠️ Potential issue | 🟠 Major

The new CLI only exposes a subset of the auth providers added in this PR.

validate and health can select only auth0, clerk, supabase, firebase, workos, and better-auth, and the shared option builder only wires flags for that same subset. The PR adds Cognito, Keycloak, OAuth2, JWT, and Custom too, so the CLI cannot validate or health-check a large chunk of the provider matrix. Derive the choices/config from the auth registry, or add the missing providers and their required options here.

Also applies to: 324-338

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/factories/authCommandFactory.ts` around lines 263 - 318, The
AUTH_PROVIDER_CHOICES constant and buildProviderOptions method only include a
subset of providers; update AUTH_PROVIDER_CHOICES to include the new providers
(e.g., "cognito", "keycloak", "oauth2", "jwt", "custom") and extend
buildProviderOptions to add the flags needed by those providers (e.g.,
pool/client ids and secrets for Cognito, realm/client secret/issuer for
Keycloak, token/issuer/alg for JWT, and generic
client_id/client_secret/authorize/token endpoints for OAuth2/custom), or replace
the hardcoded AUTH_PROVIDER_CHOICES and the options wiring inside
buildProviderOptions with a dynamic derivation from the auth registry so choices
and required flags come from the central provider registry used elsewhere.
test/continuous-test-suite-auth.ts-225-305 (1)

225-305: ⚠️ Potential issue | 🟠 Major

These helper fixtures already blow the test warning budget.

These two harness sections alone introduce well over 10 explicit anys. Since test/ only gets 10 ESLint warnings total, this single file can make pnpm run check:all fail before the auth assertions even run.

As per coding guidelines, CI pipeline enforces max 300 warnings for src/, 10 warnings for test/ via ESLint. Validate before PR submission with pnpm run check:all.

Also applies to: 508-621

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/continuous-test-suite-auth.ts` around lines 225 - 305, Tests exceed the
allowed ESLint warnings because this fixture uses many explicit any types;
replace them with concrete types and narrow casts. Define an AuthUser interface
and a RequestContext type and use those for ctx and ctx.user instead of any;
update the bearer middleware call and the handler invocation to use the
RequestContext type (referencing createBearerAuthMiddleware and
bearerMiddleware.handler), replace c.set("user" as any, ...) and c.get("user" as
any) with typed c.set<User> and c.get<User> (or the framework's generic
accessors), type the headers map as Record<string,string> without any casts by
using proper iteration types, and give `body` a precise type (e.g., unknown or a
specific interface) instead of any; these changes will remove the excessive
explicit any usages throughout the auth helper block.
src/lib/auth/providers/CognitoProvider.ts-269-290 (1)

269-290: ⚠️ Potential issue | 🟠 Major

Add a timeout around JWKS fetches.

fetch(this.jwksUri) sits on the auth path with no timeout or cancellation. A slow or blackholed JWKS endpoint can stall protected requests until the platform default network timeout, so this should fail fast and ideally keep serving the last good cached JWKS on transient failures.

As per coding guidelines, Use ErrorFactory for typed errors, withTimeout for async operations, and graceful degradation with provider fallback for error handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/CognitoProvider.ts` around lines 269 - 290, Wrap the
fetch(this.jwksUri) call in withTimeout using a configurable timeout (e.g.,
this.jwksFetchTimeout) inside CognitoProvider so the JWKS request fails fast; on
timeout or transient network error, return the last cached JWKS from jwksCache
(keyed by this.jwksUri) if still valid instead of throwing, and only throw a
typed error via ErrorFactory/AuthProviderError.create when no cached JWKS
exists. Ensure the thrown error includes the original cause ({ cause: error })
and use a clear code like "JWKS_FETCH_FAILED_TIMEOUT" or reuse
"JWKS_FETCH_FAILED" via ErrorFactory to meet the typed error guideline.
test/continuous-test-suite-auth.ts-95-158 (1)

95-158: ⚠️ Potential issue | 🟠 Major

The generate/stream sections can skip on the bug they’re meant to catch.

These cases default to vertex and then convert very broad substrings like "failed to", "not found", "network", and "no providers" into SKIP. That means broken requestContext or auth handling can still produce a green run whenever the model path fails first; use a deterministic mocked provider so the assertions actually exercise auth propagation.

Based on learnings, Setup file is test/setup.ts with global AI provider mocks.

Also applies to: 756-940

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/continuous-test-suite-auth.ts` around lines 95 - 158, The tests
defaulting to "vertex" allow broad provider errors (checked by
isExpectedProviderError) to SKIP auth tests; switch the suite to use the
deterministic mock provider from the setup file so auth/generate/stream
assertions run. Concretely, change the TEST_PROVIDER default in this file
(symbol TEST_PROVIDER) to the global mock identifier used in test/setup.ts (or
require process.env.TEST_PROVIDER to be set and fail fast), and ensure
skipIfProviderError and isExpectedProviderError are not treating generic phrases
(e.g., "failed to", "not found", "network", "no providers") as SKIP for the
mocked provider paths so auth propagation code (createBearerAuthMiddleware,
createRoleAuthMiddleware, NeuroLink) is exercised deterministically.
src/lib/neurolink.ts-2995-3005 (1)

2995-3005: ⚠️ Potential issue | 🟠 Major

Put a timeout around authenticateToken().

For Auth0/OIDC/Cognito/JWKS-backed providers this call can block on network I/O. Right now a stalled auth backend can hang generate() / stream() indefinitely before any model work starts. Please wrap the validation call in withTimeout() and translate timeout failures into your auth error taxonomy.

⏱️ Proposed fix
-                const authResult = await this.authProvider.authenticateToken(
-                  options.auth.token,
-                );
+                const authResult = await withTimeout(
+                  this.authProvider.authenticateToken(options.auth.token),
+                  PROVIDER_TIMEOUTS.AUTH_MS,
+                );

As per coding guidelines, "Use ErrorFactory for typed errors, withTimeout for async operations, and graceful degradation with provider fallback for error handling."

Also applies to: 5558-5568

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 2995 - 3005, Wrap the call to
authProvider.authenticateToken (used after ensureAuthProvider in methods like
generate and stream) with withTimeout and map timeout rejections to a typed auth
error via ErrorFactory (e.g., create an AuthTimeoutError or use the existing
auth error type) so a stalled JWKS/OIDC/Auth0 call won't hang model work;
specifically, in the block after ensureAuthProvider() where
authenticateToken(options.auth.token) is awaited, replace the direct await with
withTimeout(authProvider.authenticateToken(...), <appropriate ms>) and catch a
timeout to throw the appropriate ErrorFactory-produced auth error, preserving
other auth failures as-is; apply the same pattern to the other authenticateToken
usage locations (the other authenticateToken call referenced in the diff).
src/lib/neurolink.ts-10910-10914 (1)

10910-10914: ⚠️ Potential issue | 🟠 Major

Remove the any cast and use proper type discrimination to ensure provider/config alignment.

When config is narrowed in the else branch (after if ("provider" in config)), TypeScript should enforce that the type and config fields match one of the discriminated union members. The any cast bypasses this validation, allowing mismatched provider/config pairs like { type: "auth0", config: ClerkConfig } to compile but fail only at runtime. This also violates the repo's no-explicit-any rule in src/.

The type mismatch exists because AuthProviderFactory.create() accepts AuthProviderConfig (generic config with type, required, tokenExtraction, etc.), but NeuroLinkAuthConfig specific arms use provider-specific configs like Auth0Config (with domain, clientId, etc.) that don't extend AuthProviderConfig.

Consider refactoring AuthProviderFactory.create() to accept the full union of possible configs (using conditional or overloaded signatures) instead of the generic AuthProviderConfig, so the type system can verify alignment at the call site:

// Instead of:
factory.create(config.type as AuthProviderType, config.config as any)

// Use proper type discrimination that validates provider/config pairs
factory.create(config.type, config.config)

This approach is consistent with the discriminated union pattern already used in NeuroLinkAuthConfig and would catch misconfigurations at compile time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 10910 - 10914, Remove the `as any` cast
and make the types line up so TypeScript enforces the provider/config pair:
update AuthProviderFactory.create's signature to accept the full discriminated
union (or overloads) of provider-specific config types instead of the generic
AuthProviderConfig, then call factory.create(config.type, config.config)
directly from the NeuroLinkAuthConfig branch (the code setting
this.authProvider) so the compiler validates that the config matches the
AuthProviderType; refer to symbols AuthProviderFactory.create,
AuthProviderConfig, AuthProviderType, NeuroLinkAuthConfig and the
this.authProvider assignment when making the change.
src/lib/auth/providers/clerk.ts-68-79 (1)

68-79: ⚠️ Potential issue | 🟠 Major

The JWT branch isn't bound to the configured Clerk trust material.

Lines 68-69 accept jwtKey, but Lines 77-78 always build a verifier from a hard-coded JWKS endpoint and validateJWT() only uses that verifier. The configured key never participates, so JWT validation is not actually pinned to the app-specific config.

Also applies to: 101-107

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/clerk.ts` around lines 68 - 79, The Clerk provider
currently always uses the hard-coded remote JWKS (this.jwks =
jose.createRemoteJWKSet(...)) and never binds the configured jwtKey; update
initialize() to construct the JWT verifier from the configured jwtKey when
present (falling back to the remote JWKS only if jwtKey is absent) and ensure
validateJWT() uses that constructed verifier; specifically, reference
this.jwtKey, this.jwks, initialize() and validateJWT() and implement a
conditional path that imports or builds a local JWK/JWKSet from this.jwtKey (or
uses jose.importJWK / createLocalJWKSet equivalent) so the app-specific trust
material is actually used for verification (also apply same change to the logic
referenced around lines 101-107).
src/lib/auth/providers/auth0.ts-89-99 (1)

89-99: ⚠️ Potential issue | 🟠 Major

clientId is required but never enforced when audience is absent.

Lines 89-94 make clientId mandatory, but Lines 139-142 only constrain by audience. If callers omit audience, this provider never scopes tokens to the configured app, so same-tenant tokens for other clients/APIs can pass validation. Either require audience here or add an explicit aud/azp check against clientId.

Also applies to: 139-142

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/auth0.ts` around lines 89 - 99, The code makes
clientId required but doesn't ensure tokens are scoped to that client when
audience is absent; update the Auth0 provider so either (A) require
config.audience alongside config.clientId in the constructor (throw
InvalidConfigurationError if audience is missing), or (B) keep audience optional
but add an explicit token-claim check in the token validation routine (the block
that currently inspects audience around the lines referencing this.audience) to
verify that either the token's "aud" includes this.audience or that the token's
"azp"/"aud" matches this.clientId; implement one approach consistently and use
this.clientId/this.audience and the token validation function name (e.g.,
validateToken/verifyJwt) to locate where to add the enforcement.
src/lib/auth/providers/supabase.ts-169-176 (1)

169-176: ⚠️ Potential issue | 🟠 Major

emailVerified flips to true when the field is missing.

Line 175 checks !== null, so undefined is treated as verified. Any user record without email_confirmed_at will come back with emailVerified: true.

✅ Minimal fix
-      emailVerified: userData.email_confirmed_at !== null,
+      emailVerified: Boolean(userData.email_confirmed_at),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/supabase.ts` around lines 169 - 176, The emailVerified
computation in the return object incorrectly treats undefined as verified
because it uses strict !== null; update the check for email confirmation in the
function that returns the user object (referencing userData and the
email_confirmed_at field) to a nullish check that rejects both null and
undefined (e.g., use a != null or explicit !== undefined && !== null check) so
emailVerified is true only when email_confirmed_at has a real value.
src/lib/auth/providers/custom.ts-113-127 (1)

113-127: ⚠️ Potential issue | 🟠 Major

Don't create a local session after the custom hook fails.

If createSessionFn rejects because the external session store is down or intentionally denies login, Lines 125-127 silently fall back to a new in-memory session. That turns a failed custom session creation into a successful login.

🛑 Safer behavior
       } catch (error) {
-        logger.warn("Custom createSession failed, using default:", error);
+        logger.error("Custom createSession failed:", error);
+        throw error instanceof Error ? error : new Error(String(error));
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/custom.ts` around lines 113 - 127, The catch block
around this.createSessionFn should NOT silently fall through to the default
in-memory session; update the error handling in the method that calls
createSessionFn so that on error it either rethrows the error or returns a
failed result (e.g., null/undefined) instead of allowing the code that follows
to create and register a local session; specifically modify the catch in the
create-session flow that references this.createSessionFn, remove the fallback
that calls this.sessions.set(...), this.userSessions.get(...).add(...), and
this.emit("auth:login", user) when the custom hook fails, and ensure the
function returns/throws immediately so failed external session creation does not
become a successful in-memory login.
src/lib/auth/providers/jwt.ts-84-85 (1)

84-85: ⚠️ Potential issue | 🟠 Major

Pass config.algorithms to jwtVerify() to enforce consistent algorithm policy.

Lines 84–85 configure an allowed algorithm allow-list, but lines 133–146 never pass it to jwtVerify(). This allows the verification to accept any algorithm compatible with the key, enabling algorithm confusion attacks. Add verifyOptions.algorithms = this.algorithms; before calling jwtVerify().

Also applies to: 133–146

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/jwt.ts` around lines 84 - 85, The allowed-algorithms
list initialized on this.algorithms is never passed into jwtVerify, allowing
algorithm confusion; before calling jwtVerify(...) in the verification routine
(the block that constructs verifyOptions and then calls jwtVerify), set
verifyOptions.algorithms = this.algorithms so jwtVerify enforces the configured
allow-list; update the code path that builds verifyOptions and calls jwtVerify
to include this assignment (refer to this.algorithms, verifyOptions, and
jwtVerify).
src/lib/auth/sessionManager.ts-277-288 (1)

277-288: ⚠️ Potential issue | 🟠 Major

Avoid using Redis KEYS command in production.

The KEYS command with a pattern (${this.prefix}*) scans all keys in Redis and can block the server for seconds or more with large datasets. This is a well-documented anti-pattern for production Redis usage.

🔧 Recommended fix using SCAN
 async clear(): Promise<void> {
   try {
     const client = await this.getClient();
-    const keys = await client.keys(`${this.prefix}*`);
-
-    if (keys.length > 0) {
-      await client.del(...keys);
-    }
+    // Use SCAN with cursor-based iteration for production safety
+    let cursor = "0";
+    do {
+      const [nextCursor, keys] = await client.scan(cursor, "MATCH", `${this.prefix}*`, "COUNT", 100);
+      cursor = nextCursor;
+      if (keys.length > 0) {
+        await client.del(...keys);
+      }
+    } while (cursor !== "0");
   } catch (error) {
     logger.error("Redis clear error:", error);
   }
 }

Note: This requires adding scan to the RedisClient interface.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/sessionManager.ts` around lines 277 - 288, The clear() method
uses Redis KEYS which is unsafe for production; replace it with an incremental
SCAN loop: update the RedisClient interface to include scan(cursor: string,
options?: { MATCH?: string, COUNT?: number }) (or the client-specific scan
signature), then in clear() call getClient() and repeatedly call scan starting
with cursor "0", using MATCH `${this.prefix}*` and a reasonable COUNT, collect
matched keys into a batch and call client.del(...) for each batch (or pipeline)
until the scan cursor returns "0"; keep existing try/catch and use logger.error
on failure. Ensure you reference the clear method, getClient(), this.prefix and
the RedisClient.scan addition when making the changes.
src/lib/auth/providers/betterAuth.ts-137-143 (1)

137-143: ⚠️ Potential issue | 🟠 Major

Add timeout to external HTTP call to prevent hanging requests.

The validateSession method makes an external HTTP request to the Better Auth API without a timeout. If the Better Auth server is slow or unresponsive, this could cause requests to hang indefinitely, degrading user experience and potentially exhausting resources.

🛡️ Proposed fix to add timeout
 private async validateSession(
   sessionToken: string,
 ): Promise<TokenValidationResult> {
   try {
     const proxyFetch = createProxyFetch();
-    const response = await proxyFetch(`${this.baseUrl}/api/auth/session`, {
-      headers: {
-        Cookie: `better-auth.session_token=${sessionToken}`,
-      },
-    });
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout
+    
+    try {
+      const response = await proxyFetch(`${this.baseUrl}/api/auth/session`, {
+        headers: {
+          Cookie: `better-auth.session_token=${sessionToken}`,
+        },
+        signal: controller.signal,
+      });
+      clearTimeout(timeoutId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/betterAuth.ts` around lines 137 - 143, The
validateSession method calls createProxyFetch to GET
`${this.baseUrl}/api/auth/session` with a Cookie but has no timeout; wrap the
fetch call in an AbortController and set a short timeout (e.g., 5s) that calls
controller.abort(), pass controller.signal to proxyFetch, clear the timeout
after response, and handle AbortError by treating the session as invalid or
returning an explicit timeout error; update validateSession (and any related
error handling around response) to ensure the timeout is applied and resources
are cleaned up.
src/cli/commands/authProviders.ts-480-482 (1)

480-482: ⚠️ Potential issue | 🟠 Major

Missing provider implementations in buildProviderConfig.

The PROVIDER_INFO catalog includes oauth2, cognito, keycloak, and jwt providers, but buildProviderConfig has no cases for them. Users attempting to validate tokens or check health for these providers will always get "Missing required configuration" errors.

Do you want me to generate the missing cases for these providers?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/commands/authProviders.ts` around lines 480 - 482,
buildProviderConfig currently returns null for unhandled providers, causing
"Missing required configuration" for oauth2, cognito, keycloak, and jwt from
PROVIDER_INFO; add explicit cases in the switch inside buildProviderConfig for
'oauth2', 'cognito', 'keycloak', and 'jwt' that validate and return the
provider-specific config object (e.g., expected fields like issuer, clientId,
clientSecret, jwksUri, region, userPoolId, realm, tokenEndpoint, publicKey,
algorithms) and throw or return clear errors when required fields are missing;
reference the PROVIDER_INFO entries to mirror required keys and reuse any
existing helpers (e.g., parseUrl/validateString) and ensure the returned config
shape matches what validateToken and health-check functions expect.
src/lib/auth/providers/workos.ts-145-158 (1)

145-158: ⚠️ Potential issue | 🟠 Major

Add timeout to session validation API call.

The validateSession method makes an external HTTP POST to WorkOS without a timeout, risking hung requests if the API is unresponsive.

🛡️ Proposed fix
     try {
       const proxyFetch = createProxyFetch();
+      const controller = new AbortController();
+      const timeoutId = setTimeout(() => controller.abort(), 10000);
+      
       const response = await proxyFetch(
         "https://api.workos.com/user_management/authenticate",
         {
           method: "POST",
           headers: {
             Authorization: `Bearer ${this.apiKey}`,
             "Content-Type": "application/json",
           },
           body: JSON.stringify({
             session_token: token,
             client_id: this.clientId,
           }),
+          signal: controller.signal,
         },
       );
+      clearTimeout(timeoutId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/workos.ts` around lines 145 - 158, validateSession
currently calls proxyFetch for WorkOS without a timeout, risking hung requests;
wrap the HTTP POST to "https://api.workos.com/user_management/authenticate" in a
cancellable timeout using an AbortController (or the existing proxyFetch timeout
option if supported) so the request is aborted after a sensible duration (e.g.,
5s). Update the proxyFetch invocation inside validateSession to create an
AbortController, pass signal to proxyFetch (or pass timeout param), start a
timer that calls controller.abort() on expiry, and clear the timer on
completion; ensure the thrown abort error is handled/translated to a timeout
error in validateSession.
src/lib/auth/providers/oauth2.ts-397-411 (1)

397-411: ⚠️ Potential issue | 🟠 Major

Add timeout to token exchange request.

The exchangeCode method makes an external HTTP POST without a timeout. This is particularly important for OAuth flows where users are waiting for completion.

🛡️ Proposed fix
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 15000);
+    
     const response = await proxyFetch(this.tokenUrl, {
       method: "POST",
       headers: {
         "Content-Type": "application/x-www-form-urlencoded",
       },
       body: body.toString(),
+      signal: controller.signal,
     });
+    clearTimeout(timeoutId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/oauth2.ts` around lines 397 - 411, exchangeCode
currently calls proxyFetch(this.tokenUrl, ...) without a timeout; wrap the
request with an AbortController and set a timeout (e.g., via setTimeout) so the
POST to this.tokenUrl is aborted after a reasonable period, clear the timeout on
success, and catch an AbortError to throw a ProviderAPIError with a descriptive
message (use "oauth2" and an appropriate status/code). Update the proxyFetch
invocation inside exchangeCode to pass signal from the AbortController and
ensure any cleanup (clearTimeout) happens before returning or rethrowing.
src/lib/auth/providers/oauth2.ts-175-191 (1)

175-191: ⚠️ Potential issue | 🟠 Major

Add timeout to userInfo endpoint call.

The validateViaUserInfo method makes an external HTTP request without a timeout, which could cause requests to hang if the identity provider is unresponsive.

🛡️ Proposed fix
 private async validateViaUserInfo(
   token: string,
 ): Promise<TokenValidationResult> {
   try {
     const proxyFetch = createProxyFetch();
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 10000);
+    
     const response = await proxyFetch(this.userInfoUrl!, {
       headers: {
         Authorization: `Bearer ${token}`,
       },
+      signal: controller.signal,
     });
+    clearTimeout(timeoutId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/oauth2.ts` around lines 175 - 191, The call to the
userInfo endpoint in validateViaUserInfo lacks a timeout and can hang; wrap the
proxyFetch(this.userInfoUrl!, ...) call with an AbortController: create an
AbortController, pass signal to proxyFetch, start a setTimeout that calls
controller.abort() after a reasonable default (e.g. 5s or configurable via
provider options), clear the timeout after the request finishes, and catch the
abort error to return { valid: false, error: 'UserInfo request timed out' } (or
include the abort/error message); update validateViaUserInfo to use the
controller/signal and handle cleanup so hung requests are prevented.
🟡 Minor comments (10)
src/lib/auth/middleware/rateLimitByUser.ts-75-80 (1)

75-80: ⚠️ Potential issue | 🟡 Minor

setInterval without unref() prevents graceful Node.js shutdown.

The cleanup interval will keep the Node.js event loop alive even when all other work is done, preventing clean process exit. Call unref() on the interval handle:

   constructor(cleanupIntervalMs: number = 60000) {
     // Periodically cleanup expired buckets
     this.cleanupInterval = setInterval(() => {
       this.cleanupExpiredBuckets();
     }, cleanupIntervalMs);
+    // Don't block process exit
+    this.cleanupInterval.unref?.();
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/middleware/rateLimitByUser.ts` around lines 75 - 80, The
interval created in the constructor using setInterval keeps the Node.js event
loop alive; update the constructor to call unref() on the interval handle
(this.cleanupInterval) after creation so it doesn't block process exit — locate
the constructor in rateLimitByUser.ts where setInterval(...) invokes
this.cleanupExpiredBuckets() and call this.cleanupInterval.unref() (or otherwise
unref the returned timer) immediately after assigning it.
test/continuous-test-suite-auth.ts-11-15 (1)

11-15: ⚠️ Potential issue | 🟡 Minor

Fix linting violations for explicit any type annotations.

This file contains 25 explicit any annotations spread across helper functions (lines 226–235, 264, 279, 292, 304–305, 368, 384, 415, 431, 509–520, 539, 552, 565, 575, 621, 739). The CI pipeline enforces a maximum of 10 warnings for test files. Refactor to use proper types or suppress specific uses with // @ts-expect-error`` comments where type safety cannot be achieved.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/continuous-test-suite-auth.ts` around lines 11 - 15, Multiple helper
variables and parameters in test/continuous-test-suite-auth.ts are declared with
explicit any (25 occurrences); replace these with precise types or safer
alternatives and add targeted ts-expect-error only where typing is impossible:
locate helper functions that construct or inspect requests/responses (e.g., any
parameters/vars in the auth helper functions and validators), change
parameter/variable types from any to concrete types such as RequestInit,
Response, HeadersInit, string, number, boolean, or a small
AuthConfig/TestContext interface, prefer unknown + runtime narrowing when
dynamic, and for the few cases that cannot be typed, add a single-line //
`@ts-expect-error` with a comment explaining why; ensure no more than 10 explicit
any usages remain by refactoring functions that create requests/responses and
the validation helpers to use proper types instead of any.
src/cli/commands/authProviders.ts-468-478 (1)

468-478: ⚠️ Potential issue | 🟡 Minor

Better Auth secret cannot be provided via CLI option.

The better-auth provider requires secret (per PROVIDER_INFO), but it can only be read from BETTER_AUTH_SECRET environment variable. Unlike other providers (e.g., secretKey for Clerk), there's no CLI option for the secret. This inconsistency may confuse users.

💡 Suggested improvement

Either add a --secret CLI option in authCommandFactory.ts, or document in the error message that the secret must be set via environment variable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/commands/authProviders.ts` around lines 468 - 478, The better-auth
branch currently only reads process.env.BETTER_AUTH_SECRET and never accepts a
CLI flag; update the CLI to accept a --secret option and make the provider
prefer argv.secret over the env var. Specifically, add a secret/--secret flag in
authCommandFactory.ts (wired into argv for auth commands) and then change the
"better-auth" case in authProviders.ts to use argv.secret ||
process.env.BETTER_AUTH_SECRET for the secret check (keeping the baseUrl logic
as-is and still returning null if neither is present). Ensure the flag name
matches any PROVIDER_INFO metadata for better-auth if present.
src/lib/auth/sessionManager.ts-148-163 (1)

148-163: ⚠️ Potential issue | 🟡 Minor

Inconsistent error handling in Redis client initialization.

The code logs a warning that it's "falling back to in-memory" but then throws an error instead of actually returning a fallback storage. This creates a confusing user experience and the warning message is misleading.

🔧 Proposed fix - either throw without misleading message or implement fallback

Option 1 - Throw with accurate message:

       } catch {
-        // Fall back to in-memory if Redis is not available
-        logger.warn(
-          "Redis not available, falling back to in-memory session storage",
-        );
-        throw new Error("Redis client not available");
+        throw new Error("Redis client not available. Install ioredis package or use memory storage.");
       }

Option 2 - Actually implement fallback (requires refactoring class structure).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/sessionManager.ts` around lines 148 - 163, The getClient method
logs "falling back to in-memory session storage" but then throws instead of
returning a fallback client; fix by either (A) making the behavior honest:
remove the misleading logger.warn and change the catch to throw a clear error
(e.g., log an error and throw new Error("Redis client not available")), or (B)
implement a real in-memory fallback: instantiate and return an in-memory
RedisClient-compatible implementation inside the catch (use this.client =
<in-memory impl> and return this.client), ensuring references to this.redisUrl,
this.client, getClient and the RedisClient type remain consistent; pick one
option and update the catch block accordingly.
src/lib/auth/providers/oauth2.ts-206-211 (1)

206-211: ⚠️ Potential issue | 🟡 Minor

Misleading tokenType: "jwt" for userInfo-based validation.

When validation is performed via the userInfo endpoint (not JWT verification), the response incorrectly reports tokenType: "jwt". This should be "access_token" or "opaque" to accurately reflect that no JWT was validated.

🔧 Proposed fix
       return {
         valid: true,
         payload: data,
         user,
-        tokenType: "jwt",
+        tokenType: "access_token",
       };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/oauth2.ts` around lines 206 - 211, The returned auth
result in src/lib/auth/providers/oauth2.ts currently sets tokenType: "jwt" even
in the userInfo-based validation path; update the userInfo validation code path
(the function that returns the object with properties valid, payload, user,
tokenType) to set tokenType to "access_token" (or "opaque" if you prefer that
terminology) when the token was validated via the userInfo endpoint rather than
JWT verification, and keep tokenType "jwt" only for the JWT verification branch
so callers can distinguish validation types.
src/lib/auth/providers/workos.ts-374-380 (1)

374-380: ⚠️ Potential issue | 🟡 Minor

Avoid logging full error objects that may contain sensitive data.

Logging the complete error object could expose sensitive information like API keys or user data in stack traces.

🛡️ Proposed fix
     } catch (error) {
-      logger.error("Failed to fetch WorkOS user:", error);
+      logger.error("Failed to fetch WorkOS user:", {
+        userId,
+        error: error instanceof Error ? error.message : String(error),
+      });
       if (error instanceof ProviderAPIError) {
         throw error;
       }
       return null;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/workos.ts` around lines 374 - 380, Replace the current
logger.error call that logs the full error object in the catch block of the
WorkOS user fetch (the block handling errors around ProviderAPIError) with a
sanitized log that only records non-sensitive information (e.g., a descriptive
message plus error.message and any safe, non-secret fields like error.code or
status if present); keep the existing instanceof ProviderAPIError rethrow
behavior unchanged and do not include stack traces or raw error objects in logs.
src/lib/auth/providers/betterAuth.ts-317-339 (1)

317-339: ⚠️ Potential issue | 🟡 Minor

Add timeout to health check HTTP call.

Similar to validateSession, the health check makes an external call without a timeout. Health checks should be bounded to prevent cascading timeouts.

🛡️ Proposed fix
 async healthCheck(): Promise<AuthHealthCheck> {
   try {
     const proxyFetch = createProxyFetch();
-    // Better Auth typically exposes session endpoint that we can check
-    const response = await proxyFetch(`${this.baseUrl}/api/auth/session`);
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 5000);
+    
+    const response = await proxyFetch(`${this.baseUrl}/api/auth/session`, {
+      signal: controller.signal,
+    });
+    clearTimeout(timeoutId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/betterAuth.ts` around lines 317 - 339, The healthCheck
method in betterAuth.ts calls
createProxyFetch(`${this.baseUrl}/api/auth/session`) without a timeout; modify
healthCheck to use the same timeout pattern as validateSession by creating an
AbortController, set a short timeout (e.g., 2–5s) to call controller.abort(),
pass the controller.signal into the proxyFetch call, and ensure the abort timer
is cleared on success; update error handling to treat an AbortError as an
unhealthy/service-timeout case while preserving the existing response status
logic in healthCheck.
src/lib/auth/providers/oauth2.ts-139-147 (1)

139-147: ⚠️ Potential issue | 🟡 Minor

Empty string fallback for user ID could cause downstream issues.

If payload.sub is undefined, the user ID becomes an empty string (""), which could cause problems with session tracking, database lookups, or security checks that expect valid identifiers.

🛡️ Proposed fix

Consider returning an invalid result if sub is missing:

+      if (!payload.sub) {
+        return {
+          valid: false,
+          error: "JWT missing required 'sub' claim",
+        };
+      }
+
       const user: AuthUser = {
-        id: payload.sub ?? "",
+        id: payload.sub,
         email: payload.email as string | undefined,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/oauth2.ts` around lines 139 - 147, The code currently
constructs an AuthUser with id: payload.sub ?? "" which allows an empty-string
ID; change the logic in the oauth2 provider where the AuthUser is created (the
block that builds AuthUser) to validate payload.sub first and return/raise an
invalid authentication result instead of creating a user with an empty id.
Specifically, check payload.sub (and optionally payload.sub.trim()) before
building AuthUser, and if it's missing/empty, return a failure (or throw a clear
error) from the function that processes the token so callers do not receive an
AuthUser with an empty id.
src/lib/auth/AuthProviderRegistry.ts-403-422 (1)

403-422: ⚠️ Potential issue | 🟡 Minor

Weak health check implementation.

The health check only verifies that provider.type === type, which is essentially checking if the provider was created with the correct type. This doesn't actually test connectivity or provider health. Most providers implement a healthCheck() method that should be called instead.

🔧 Proposed fix to use provider's healthCheck method
     try {
       const provider = await this.createProvider(type, config);
 
-      // Simple health check - try to access config
-      const healthy = provider.type === type;
+      // Use provider's health check if available
+      let healthy = true;
+      if (typeof provider.healthCheck === "function") {
+        const healthResult = await provider.healthCheck();
+        healthy = healthResult.healthy;
+      }
 
       // Clean up if provider has dispose method
-      if (provider.dispose) {
+      if (typeof provider.dispose === "function") {
         await provider.dispose();
+      } else if (typeof provider.cleanup === "function") {
+        await provider.cleanup();
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/AuthProviderRegistry.ts` around lines 403 - 422, The current
health check only compares provider.type and must instead call the provider's
healthCheck method to verify real connectivity; in the health check flow where
you call this.createProvider(type, config) (and later provider.dispose), invoke
await provider.healthCheck() if present and use its boolean/result to set the
ProviderHealthStatus. Maintain cleanup by still calling provider.dispose() when
defined, compute latency from startTime, populate ProviderHealthStatus (type,
healthy from healthCheck, lastCheck, latency) and update
this.healthCache.set(type, status) before returning.
src/lib/auth/types/authTypes.ts-719-724 (1)

719-724: ⚠️ Potential issue | 🟡 Minor

Interface signature mismatch for authenticateToken.

The MastraAuthProvider interface defines authenticateToken(token: string) without a context parameter, but BaseAuthProvider declares it as authenticateToken(token: string, context?: AuthRequestContext). This inconsistency could cause issues for providers that need request context.

🔧 Proposed fix in authTypes.ts
   /**
    * Validate and authenticate a token
    * `@param` token - JWT or access token to validate
+   * `@param` context - Optional request context for additional validation
    * `@returns` Validation result with user data if valid
    */
-  authenticateToken(token: string): Promise<TokenValidationResult>;
+  authenticateToken(
+    token: string,
+    context?: AuthRequestContext,
+  ): Promise<TokenValidationResult>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/types/authTypes.ts` around lines 719 - 724, The interface
signature for authenticateToken in MastraAuthProvider doesn't include the
optional context parameter and must match BaseAuthProvider; update
MastraAuthProvider.authenticateToken to accept (token: string, context?:
AuthRequestContext): Promise<TokenValidationResult> so implementations can
receive the optional AuthRequestContext; ensure you import or reference
AuthRequestContext and TokenValidationResult types used by BaseAuthProvider and
keep the return type as Promise<TokenValidationResult>.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7ecf3bf4-16d1-4794-99b5-e935fdab35c4

📥 Commits

Reviewing files that changed from the base of the PR and between 86b0a32 and 0c52a72.

📒 Files selected for processing (43)
  • docs/superpowers/plans/2026-03-16-auth-providers-implementation.md
  • docs/superpowers/specs/2026-03-16-auth-providers-redesign.md
  • package.json
  • src/cli/commands/authProviders.ts
  • src/cli/factories/authCommandFactory.ts
  • src/cli/parser.ts
  • src/lib/auth/AuthProviderFactory.ts
  • src/lib/auth/AuthProviderRegistry.ts
  • src/lib/auth/RequestContext.ts
  • src/lib/auth/authContext.ts
  • src/lib/auth/authErrors.ts
  • src/lib/auth/authProvider.ts
  • src/lib/auth/index.ts
  • src/lib/auth/middleware/AuthMiddleware.ts
  • src/lib/auth/middleware/rateLimitByUser.ts
  • src/lib/auth/providers/BaseAuthProvider.ts
  • src/lib/auth/providers/CognitoProvider.ts
  • src/lib/auth/providers/KeycloakProvider.ts
  • src/lib/auth/providers/auth0.ts
  • src/lib/auth/providers/betterAuth.ts
  • src/lib/auth/providers/clerk.ts
  • src/lib/auth/providers/custom.ts
  • src/lib/auth/providers/firebase.ts
  • src/lib/auth/providers/jwt.ts
  • src/lib/auth/providers/oauth2.ts
  • src/lib/auth/providers/supabase.ts
  • src/lib/auth/providers/workos.ts
  • src/lib/auth/serverBridge.ts
  • src/lib/auth/sessionManager.ts
  • src/lib/auth/types/authTypes.ts
  • src/lib/index.ts
  • src/lib/mcp/toolRegistry.ts
  • src/lib/neurolink.ts
  • src/lib/server/routes/agentRoutes.ts
  • src/lib/types/authTypes.ts
  • src/lib/types/configTypes.ts
  • src/lib/types/generateTypes.ts
  • src/lib/types/index.ts
  • src/lib/types/streamTypes.ts
  • test/continuous-test-suite-auth.ts
  • test/fixtures/auth/provider-config.json
  • test/fixtures/auth/session-data.json
  • test/fixtures/auth/test-credentials.json

Comment on lines +310 to +345
// Handle per-call auth token validation
if (enhancedOptions.auth?.token) {
await this.ensureAuthProvider();
if (!this.authProvider) {
throw new Error(
"No auth provider configured. Set auth in constructor or via setAuthProvider().",
);
}
const result = await this.authProvider.authenticateToken(
enhancedOptions.auth.token,
);
if (!result.valid) {
const { InvalidTokenError } = await import("./auth/authErrors.js");
throw new InvalidTokenError(
result.error || "Token validation failed",
this.authProvider.type,
);
}
// Merge validated user into context
if (result.user) {
enhancedOptions.context = {
...enhancedOptions.context,
userId: result.user.id,
userEmail: result.user.email,
userRoles: result.user.roles,
};
}
}

// Handle pre-validated requestContext
if (enhancedOptions.requestContext) {
enhancedOptions.context = {
...enhancedOptions.context,
...enhancedOptions.requestContext,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Merge requestContext before token-derived identity.

If callers provide both auth.token and requestContext, the second spread here can overwrite userId, userEmail, and userRoles that were just derived from a validated token. That collapses the trust boundary the reserved-key design is trying to create unless these auth-owned keys are merged last or explicitly filtered out.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-03-16-auth-providers-implementation.md` around
lines 310 - 345, Currently requestContext is merged after token-derived
identity, allowing it to overwrite userId/userEmail/userRoles; change the merge
order or filter reserved keys so token-derived identity wins: when handling
enhancedOptions.requestContext, either spread requestContext into
enhancedOptions.context before the token-derived merge (move the requestContext
block above the token validation) or remove reserved keys (userId, userEmail,
userRoles) from enhancedOptions.requestContext before spreading; reference
ensureAuthProvider, authProvider.authenticateToken, and the
enhancedOptions.context merges to locate where to reorder or filter.

Comment thread package.json
Comment thread src/lib/auth/providers/CognitoProvider.ts
Comment thread src/lib/auth/providers/KeycloakProvider.ts
Comment thread src/lib/auth/types/authTypes.ts Outdated
Comment thread src/lib/neurolink.ts
Comment thread src/lib/neurolink.ts Outdated
Comment on lines 70 to 76
context: {
sessionId: request.sessionId,
userId: request.userId,
sessionId: ctx.session?.id ?? request.sessionId,
userId: ctx.user?.id ?? request.userId,
userEmail: ctx.user?.email,
userRoles: ctx.user?.roles,
requestId: ctx.requestId,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don't promote caller-supplied IDs into trusted auth context.

request.userId and request.sessionId come from the body, but these blocks forward them as context.userId / context.sessionId whenever ctx.user or ctx.session is missing. That gives unauthenticated callers a direct impersonation path for any downstream logic that keys memory, tools, rate limiting, or auditing off the execution context. Only populate identity from ctx.user / ctx.session; if you still need the raw request values, keep them under separate untrusted fields.

Suggested hardening
             context: {
-              sessionId: ctx.session?.id ?? request.sessionId,
-              userId: ctx.user?.id ?? request.userId,
+              sessionId: ctx.session?.id,
+              userId: ctx.user?.id,
               userEmail: ctx.user?.email,
               userRoles: ctx.user?.roles,
               requestId: ctx.requestId,
+              requestedSessionId: request.sessionId,
+              requestedUserId: request.userId,
             },

Also applies to: 133-139

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/server/routes/agentRoutes.ts` around lines 70 - 76, The context
object in agentRoutes.ts is promoting caller-supplied IDs into the trusted
execution context by using request.sessionId and request.userId when ctx.session
or ctx.user are absent; update the logic in the context assignment (the block
that sets context.sessionId, context.userId, context.userEmail,
context.userRoles, context.requestId) to only populate sessionId and userId from
ctx.session and ctx.user respectively and never fallback to request.* for those
trusted fields, and if you still need the raw body values keep them under
clearly untrusted names (e.g., rawRequestSessionId, rawRequestUserId) so
downstream code uses only ctx.user/ctx.session for authentication/authorization;
apply the same change in the analogous block around the 133-139 region.

Comment thread src/lib/types/streamTypes.ts
@murdore

murdore commented Mar 21, 2026

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed (Cycle 1)

Changes Made

All 11 Copilot review comments + CI build failure resolved:

CI Fix

  • pnpm-lock.yaml — Regenerated lockfile to include jose@^6.1.3 dependency. Fixes ERR_PNPM_OUTDATED_LOCKFILE in build-check, test, and Code Quality gates.

Security Fixes (2)

  • [neurolink.ts:~3031] — @Copilot: privilege escalation via requestContext override in generate()

    • Fix: Token-derived identity fields (userId, userEmail, userRoles) now take precedence over requestContext when both auth.token and requestContext are provided. Token-validated values are re-applied after requestContext merge.
    • Verification: TypeScript ✅, no logic change when only requestContext is used (no token).
  • [neurolink.ts:~5595] — @Copilot: same privilege escalation in stream()

    • Fix: Same precedence fix applied to stream() method.
    • Verification: TypeScript ✅

Architecture/Consistency Fixes (9)

  • [AuthProviderRegistry.ts:16] — @Copilot: mixed auth type imports

    • Fix: Standardized both AuthProviderFactory and AuthProviderRegistry to import from ../types/authTypes.js (top-level canonical types), matching BaseAuthProvider's implementation target.
  • [authCommandFactory.ts:270] — @Copilot: AUTH_PROVIDER_CHOICES mismatch

    • Fix: Added jwt, oauth2, cognito, keycloak to AUTH_PROVIDER_CHOICES array so CLI accepts all supported providers.
  • [neurolink.ts:10906] — @Copilot: setAuthProvider doesn't accept direct instances

    • Fix: Added duck-type check (authenticateToken method detection) to accept bare MastraAuthProvider instances directly, without wrapper.
  • [authProvider.ts:91] — @Copilot: case-sensitive header lookup

    • Fix: Replaced direct context.headers[headerName] lookup with case-insensitive Object.entries() iteration matching AuthMiddleware.extractToken pattern.
  • [AuthProviderRegistry.ts:412] — @Copilot: checkProviderHealth() doesn't call healthCheck()

    • Fix: Now calls provider.healthCheck() when available, falling back to type comparison. Health errors included in returned status.
  • [authProviders.ts:477] — @Copilot: missing type field in buildProviderConfig()

    • Fix: Added type: argv.provider to every config object returned by buildProviderConfig().
  • [sessionManager.ts:157] — @Copilot: misleading Redis fallback log

    • Fix: Changed logger.warn("Redis not available, falling back to in-memory session storage") to logger.error(...) with accurate message since code throws rather than falls back.
  • [configTypes.ts:118] — @Copilot: NeuroLinkAuthConfig type doesn't accept direct instances

    • Fix: Widened NeuroLinkAuthConfig union to include bare MastraAuthProvider as first member.
  • [index.ts:1320] — @Copilot: duplicate auth type exports

    • Fix: Removed redundant re-exports already covered by export * from "./types/index.js". Kept only 3 types unique to auth module (AuthMiddlewareConfig, TokenClaims, MastraAuthProvider).

Files Modified

  • pnpm-lock.yaml — Regenerated for jose dependency
  • src/lib/neurolink.ts — Security fix (token precedence), setAuthProvider duck-typing
  • src/lib/auth/AuthProviderFactory.ts — Standardized type imports
  • src/lib/auth/AuthProviderRegistry.ts — Standardized type imports, healthCheck() call, dispose duck-type
  • src/lib/auth/authProvider.ts — Case-insensitive header lookup
  • src/lib/auth/sessionManager.ts — Accurate Redis error log
  • src/lib/types/configTypes.ts — Widened NeuroLinkAuthConfig type
  • src/lib/index.ts — Removed duplicate exports
  • src/cli/factories/authCommandFactory.ts — Full provider choices list
  • src/cli/commands/authProviders.ts — Added type field to configs

Validation

  • TypeScript: ✅ (1 pre-existing ioredis module warning only)
  • ESLint: ✅ (0 errors in auth files)
  • Lockfile: ✅ (jose resolved)

Requesting Re-review

@copilot-pull-request-reviewer — All 11 inline comments addressed. Please re-review.

Copilot AI commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

@murdore I've opened a new pull request, #893, to work on those changes. Once the pull request is ready, I'll request review from you.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@murdore

murdore commented Mar 21, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 21, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@murdore

murdore commented Mar 21, 2026

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed (Cycle 2 — CodeRabbit)

Critical Security Fixes (3)

  • [CognitoProvider.ts:252] — Forged JWT acceptance

    • Fix: verifySignature() now uses jose.importJWK() + jose.jwtVerify() to cryptographically verify the JWT signature against the public key, not just check kid existence.
  • [KeycloakProvider.ts:184] — Same forged JWT vulnerability

    • Fix: Identical importJWK + jwtVerify fix applied.
  • [agentRoutes.ts:76,133] — Caller-supplied ID impersonation

    • Fix: When ctx.user exists (from auth middleware), userId/sessionId are always taken from the authenticated context. Request body values only used as fallback when no auth middleware is active.

High Priority Fixes (6)

  • [authTypes.ts:82] — AuthSession.expiresAt type mismatch

    • Fix: Made expiresAt optional (expiresAt?: Date). Added null guards in BaseAuthProvider.ts for expiration checks.
  • [jwt.ts:84] — Algorithm confusion attack

    • Fix: config.algorithms now passed to jwtVerify() options to enforce algorithm policy.
  • [supabase.ts:175] — emailVerified false positive

    • Fix: Changed !== null to !! to treat both null and undefined as unverified.
  • [auth0.ts:146] — No aud/azp claim validation

    • Fix: Added post-verification checks: aud validated against config.audience, azp validated against config.clientId.
  • [custom.ts:113] — Silent session fallback after hook failure

    • Fix: Removed try/catch that swallowed user-provided createSession hook errors. Errors now propagate.
  • [sessionManager.ts:277] — Redis KEYS command in production

    • Fix: Replaced KEYS with iterative SCAN (COUNT 100) to avoid blocking Redis.

Minor Fixes (1)

  • [rateLimitByUser.ts:77] — setInterval without unref()
    • Fix: Added .unref() to cleanup timer to allow graceful Node.js shutdown.

Also Fixed

  • CI formatting — Ran prettier on 4 files that agents edited without formatting
  • CI ioredis module — Used variable indirection for dynamic import to avoid TS2307 at compile time

Validation

  • TypeScript: ✅ (0 errors)
  • ESLint: ✅ (0 errors in changed files)
  • Prettier: ✅

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 21, 2026

Copy link
Copy Markdown

@murdore Excellent work on Cycle 2 — the fixes address all the critical and high-priority issues raised. A few highlights worth noting:

  • The JWT signature verification fix (using jose.importJWK() + jose.jwtVerify() in both CognitoProvider and KeycloakProvider) is exactly the right approach — moving from a kid existence check to actual cryptographic verification closes a serious forged-token vulnerability.
  • The algorithm confusion fix in jwt.ts (config.algorithms passed to jwtVerify) is critical for preventing alg: none and HMAC/RSA confusion attacks.
  • The Redis KEYS → SCAN replacement in sessionManager.ts is an important production safety fix.
  • The agentRoutes impersonation fix (authenticated context always wins over request body) is the correct precedence model for a multi-tenant system.

Triggering an incremental review now to inspect the Cycle 2 changes.


🧠 Learnings used
Learnt from: CR
Repo: juspay/neurolink PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T04:58:08.495Z
Learning: Applies to src/lib/providers/**/*.ts : All 12+ providers must support consistent core interface. When implementing new providers, ensure compatibility with existing provider patterns and test provider consistency across all providers.

Learnt from: CR
Repo: juspay/neurolink PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T04:58:08.495Z
Learning: Applies to src/lib/providers/**/*.ts : Providers must support consistent core interface across all 12+ providers (OpenAI, Anthropic, Google AI Studio, AWS Bedrock, Azure, Vertex, Mistral, LiteLLM, SageMaker, Hugging Face, Ollama, OpenAI-compatible).
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cli/factories/authCommandFactory.ts (1)

1-1: ⚠️ Potential issue | 🟡 Minor

Address Prettier formatting issues flagged by CI.

The pipeline reports formatting issues in this file. Run pnpm format or your formatter to resolve before merge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/factories/authCommandFactory.ts` at line 1, Prettier formatting
errors are present in src/cli/factories/authCommandFactory.ts; run the project
formatter (pnpm format) or your configured Prettier to reformat this file, then
re-stage the changes. Locate the file and ensure exported symbols such as
authCommandFactory or createAuthCommand (and any surrounding JSDoc/header
comments) are properly formatted according to the repo Prettier settings, fix
any trailing whitespace or missing semicolons reported by CI, and commit the
formatted file so the pipeline passes.
♻️ Duplicate comments (4)
src/lib/server/routes/agentRoutes.ts (1)

70-76: ⚠️ Potential issue | 🔴 Critical

Security: Caller-supplied IDs still fall back to request body values.

The previous review correctly identified this as a privilege escalation vector. While the PR summary mentions fixing token precedence in neurolink.ts, this route still falls back to request.sessionId/request.userId when ctx.session/ctx.user are missing, allowing unauthenticated callers to impersonate users in downstream logic (memory, tools, rate limiting, auditing).

🔒 Recommended fix
             context: {
-              sessionId: ctx.session?.id ?? request.sessionId,
-              userId: ctx.user?.id ?? request.userId,
+              sessionId: ctx.session?.id,
+              userId: ctx.user?.id,
               userEmail: ctx.user?.email,
               userRoles: ctx.user?.roles,
               requestId: ctx.requestId,
             },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/server/routes/agentRoutes.ts` around lines 70 - 76, The context
construction in agentRoutes.ts currently falls back to caller-supplied values
(request.sessionId/request.userId), enabling impersonation; change the context
to use only authenticated values (ctx.session?.id and ctx.user?.id) without
falling back to request.sessionId or request.userId, i.e., remove the "??
request.sessionId" and "?? request.userId" fallbacks for sessionId and userId
(leave them undefined when ctx lacks them), and ensure
authentication/authorization is enforced earlier (e.g., return 401 or validate
in the calling middleware) rather than relying on request body IDs.
src/lib/auth/providers/CognitoProvider.ts (1)

241-257: ⚠️ Potential issue | 🔴 Critical

verifySignature() does not perform cryptographic verification - security bypass.

This was flagged in a previous review and remains unfixed. After finding a matching kid, the method returns true without verifying the signature. Any token with valid structure and a copied kid will pass authentication.

The jose library is already a project dependency. Use it to properly verify the token.

🔒 Proposed fix using jose library
+import * as jose from "jose";
+
 // In verifySignature method:
       // Get JWKS
       const jwks = await this.getJWKS();
       const key = jwks.keys.find((k) => k.kid === kid);

       if (!key) {
         logger.warn(`[CognitoProvider] Key not found for kid: ${kid}`);
         return false;
       }

-      // For full verification, use jose library
-      // For now, we trust the token structure
-      return true;
+      // Import the JWK and verify the signature
+      const publicKey = await jose.importJWK(key, header.alg);
+      await jose.jwtVerify(token, publicKey, {
+        issuer: this.expectedIssuer,
+        audience: this.cognitoConfig.clientId,
+      });
+      return true;
     } catch (error) {
       logger.error(`[CognitoProvider] Signature verification error:`, error);
       return false;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/CognitoProvider.ts` around lines 241 - 257, The
verifySignature method currently returns true after finding a matching kid
without cryptographic verification; update verifySignature to use the jose
library to actually verify the token signature: use getJWKS() to find the
matching JWK for the token's kid, convert/import that JWK (via jose importJWK or
createRemoteJWKSet) and call jose.jwtVerify (or appropriate verifier) against
the raw token and the imported key, ensure you validate alg/exp/iss/aud as
needed, return true only on successful verification and false on any
verification error, and keep the existing logger.error path for exceptions;
refer to verifySignature, getJWKS, and the kid/token variables to locate where
to insert the jose-based verification.
src/lib/neurolink.ts (2)

3015-3048: ⚠️ Potential issue | 🟠 Major

Do not overwrite requestContext identity with undefined token fields.

Line 3020/3021 and Line 5600/5601 can carry undefined, and Line 3047 / Line 5627 spreads those values last. That can erase valid requestContext.userEmail / userRoles.

🛠️ Suggested fix (copy only defined token-derived fields)
-const tokenDerivedFields =
-  options.auth?.token && this.authProvider
-    ? {
-        userId: (options.context as Record<string, unknown> | undefined)?.userId,
-        userEmail: (options.context as Record<string, unknown> | undefined)?.userEmail,
-        userRoles: (options.context as Record<string, unknown> | undefined)?.userRoles,
-      }
-    : {};
+const tokenDerivedFields: Record<string, unknown> = {};
+if (options.auth?.token && this.authProvider) {
+  const ctx = (options.context as Record<string, unknown> | undefined) ?? {};
+  if (ctx.userId !== undefined) tokenDerivedFields.userId = ctx.userId;
+  if (ctx.userEmail !== undefined) tokenDerivedFields.userEmail = ctx.userEmail;
+  if (ctx.userRoles !== undefined) tokenDerivedFields.userRoles = ctx.userRoles;
+}

Apply the same change to both generate() and stream().

Also applies to: 5595-5628

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 3015 - 3048, The code is overwriting
requestContext identity fields with undefined token-derived values; update the
logic that builds tokenDerivedFields so it only includes properties that are
defined (e.g., check auth?.token && this.authProvider then conditionally add
userId, userEmail, userRoles only if they are not undefined) before spreading
into options.context; apply the same change in both generate() and stream() so
options.context = { ...(options.context || {}), ...options.requestContext,
...tokenDerivedFields } never erases valid requestContext values with undefined
token fields.

519-523: ⚠️ Potential issue | 🔴 Critical

Auth context is instance-scoped and can leak across concurrent requests.

Line 521 / Line 10993 store auth context on the NeuroLink instance, while Line 11030 exports a shared singleton. Concurrent requests can overwrite each other’s auth context.

🔒 Suggested fix (request-scoped auth context via AsyncLocalStorage)
+const authContextStorage =
+  new AsyncLocalStorage<AuthenticatedContext | undefined>();

 export class NeuroLink {
   private authProvider?: MastraAuthProvider;
-  private authContext?: AuthenticatedContext;
   private pendingAuthConfig?: NeuroLinkAuthConfig;
@@
   setAuthContext(context: AuthenticatedContext): void {
-    this.authContext = context;
+    authContextStorage.enterWith(context);
     logger.debug("Auth context set", {
       userId: context.user.id,
       provider: context.provider,
       sessionId: context.session.id,
     });
   }

   getAuthContext(): AuthenticatedContext | undefined {
-    return this.authContext;
+    return authContextStorage.getStore();
   }

   clearAuthContext(): void {
-    const userId = this.authContext?.user.id;
-    this.authContext = undefined;
+    const userId = authContextStorage.getStore()?.user.id;
+    authContextStorage.enterWith(undefined);
     if (userId) {
       logger.debug(`Auth context cleared for user: ${userId}`);
     }
   }

Also applies to: 10992-11017, 11030-11030

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 519 - 523, NeuroLink currently stores
request-specific state (authContext and pendingAuthConfig) as instance fields on
the NeuroLink class (authContext, pendingAuthConfig, and authProvider of type
MastraAuthProvider), which will leak/overwrite when the NeuroLink instance is
exported as a shared singleton; refactor to make auth state request-scoped:
remove instance fields for authContext/pendingAuthConfig (and avoid mutating
authProvider per-request) and instead use an AsyncLocalStorage store (or require
an explicit AuthenticatedContext param) to set/get authContext and
pendingAuthConfig per request; update all methods that read/write
authContext/pendingAuthConfig to pull from the AsyncLocalStorage store (or
accept the context param) so the exported shared NeuroLink singleton no longer
holds mutable per-request state.
🟠 Major comments (20)
src/lib/auth/providers/CognitoProvider.ts-269-274 (1)

269-274: ⚠️ Potential issue | 🟠 Major

Add timeout to JWKS fetch to prevent indefinite hanging.

The fetch() call has no timeout, which could cause the authentication flow to hang indefinitely if the Cognito endpoint is unresponsive.

🛡️ Proposed fix using AbortController
     try {
+      const controller = new AbortController();
+      const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout
+
-      const response = await fetch(this.jwksUri);
+      const response = await fetch(this.jwksUri, {
+        signal: controller.signal,
+      });
+      clearTimeout(timeoutId);

       if (!response.ok) {
         throw new Error(`JWKS fetch failed: ${response.status}`);
       }

As per coding guidelines: "Use withTimeout for async operations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/CognitoProvider.ts` around lines 269 - 274, The JWKS
fetch in CognitoProvider (the fetch(this.jwksUri) call) lacks a timeout; wrap
the fetch in a cancellable timeout using the project's withTimeout utility or an
AbortController so the request cannot hang indefinitely. Modify the code in the
method that uses this.jwksUri to create an AbortController, pass its signal to
fetch, and race/withTimeout the fetch so that on timeout you abort the
controller and throw a clear error (e.g., "JWKS fetch timed out") before the
existing response.ok check; ensure existing error handling/logging remains and
include the original error/timeouts in the thrown/logged message.
src/lib/neurolink.ts-10979-10985 (1)

10979-10985: ⚠️ Potential issue | 🟠 Major

ensureAuthProvider() needs in-flight deduplication.

Parallel first authenticated calls can race through Line 10980 and initialize multiple providers, because factory creation is per-call and not internally deduplicated.

♻️ Suggested fix (coalesce concurrent auth init)
 export class NeuroLink {
   private authProvider?: MastraAuthProvider;
   private pendingAuthConfig?: NeuroLinkAuthConfig;
+  private authInitPromise: Promise<void> | null = null;
@@
   private async ensureAuthProvider(): Promise<void> {
     if (this.authProvider || !this.pendingAuthConfig) {
       return;
     }
-    await this.setAuthProvider(this.pendingAuthConfig);
-    this.pendingAuthConfig = undefined;
+    if (this.authInitPromise) {
+      return this.authInitPromise;
+    }
+    this.authInitPromise = (async () => {
+      await this.setAuthProvider(this.pendingAuthConfig!);
+      this.pendingAuthConfig = undefined;
+    })();
+    try {
+      await this.authInitPromise;
+    } finally {
+      this.authInitPromise = null;
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 10979 - 10985, ensureAuthProvider can race
when multiple callers enter before authProvider is set; add an in-flight
deduplication promise (e.g., a private field like authInitPromise: Promise<void>
| undefined) so only the first caller triggers
setAuthProvider(pendingAuthConfig) and others await the same promise; implement
logic in ensureAuthProvider to return immediately if authProvider exists, if
authInitPromise exists await it, otherwise create and assign authInitPromise =
(async () => { await this.setAuthProvider(this.pendingAuthConfig);
this.pendingAuthConfig = undefined; })(), await it, then finally clear
authInitPromise (and ensure it clears on rejection) so subsequent attempts can
retry.
src/lib/neurolink.ts-10947-10955 (1)

10947-10955: ⚠️ Potential issue | 🟠 Major

Remove the any cast by properly typing the auth config parameter.

The any cast at line 10951 violates the strict TypeScript configuration requirement enforced in src/. However, this is a systemic issue—the factory's create() method signature declares it accepts AuthProviderConfig (a generic interface), but NeuroLinkAuthConfig after type narrowing is a union of specific provider configs (Auth0Config, ClerkConfig, FirebaseConfig, etc.) that don't align with the generic interface.

This same pattern repeats throughout AuthProviderFactory's internal provider registrations (Auth0, Clerk, Firebase, Supabase, Cognito, Keycloak, BetterAuth, WorkOS, Custom, OAuth2, JWT—all using as any casts).

To fix this properly:

  1. Update the factory's create() method signature to accept the union of specific provider configs instead of the generic interface, or
  2. Ensure the specific provider config types (Auth0Config, ClerkConfig, etc.) align with the generic AuthProviderConfig interface, or
  3. Add explicit type narrowing/guards to eliminate the need for casting.

This requires refactoring the auth provider type architecture to maintain strict TypeScript compliance across the system.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 10947 - 10955, The auth config is being
passed to AuthProviderFactory.create with an unsafe any cast; remove the cast by
making the factory accept the concrete union or by narrowing the config before
the call. Update AuthProviderFactory.create's signature to accept
NeuroLinkAuthConfig (the union of Auth0Config | ClerkConfig | FirebaseConfig |
SupabaseConfig | CognitoConfig | KeycloakConfig | BetterAuthConfig |
WorkOSConfig | CustomConfig | OAuth2Config | JWTConfig), or add
overloads/discriminated-union typing so calls from NeuroLinkAuthConfig (in
neurolink.ts) are type-safe, and then remove all other `as any` casts used
inside AuthProviderFactory when registering providers (e.g., the Auth0, Clerk,
Firebase, Supabase, Cognito, Keycloak, BetterAuth, WorkOS, Custom, OAuth2, JWT
registrations) by adjusting provider registration types or adding explicit type
guards to narrow to the correct provider config before instantiation.
src/lib/auth/providers/oauth2.ts-109-123 (1)

109-123: ⚠️ Potential issue | 🟠 Major

JWKS validation never initializes on first use.

authenticateToken() only enters the JWT path when this.jwks is already populated, but nothing calls initialize() before that. With jwksUrl configured and no manual pre-initialization, the first auth attempt falls straight through to "No validation method available".

🐛 Minimal fix
   async authenticateToken(
     token: string,
     _context?: AuthRequestContext,
   ): Promise<TokenValidationResult> {
-    // Try JWKS validation first if available
-    if (this.jwksUrl && this.jwks) {
+    if (this.jwksUrl && !this.jwks) {
+      await this.initialize();
+    }
+
+    if (this.jwks) {
       try {
         const { payload } = await jose.jwtVerify(token, this.jwks);

Also applies to: 130-169

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/oauth2.ts` around lines 109 - 123, The JWKS setup in
initialize() is never invoked before authenticateToken() runs, so authentication
skips JWT validation when this.jwks is unset; modify authenticateToken() (and
any other token/validation entrypoints around the jwt-handling block) to lazily
initialize JWKS by awaiting this.initialize() when this.jwks is undefined and
this.jwksUrl is present, ensuring initialize() is idempotent and returns
immediately if jwks is already set; reference initialize(), authenticateToken(),
this.jwks and jwksUrl to locate and update the logic.
src/lib/auth/providers/BaseAuthProvider.ts-425-460 (1)

425-460: ⚠️ Potential issue | 🟠 Major

RBAC wildcards and transitive inheritance are missing here.

authorize() only does exact permission matches, and getEffectiveRoles() expands the hierarchy one hop. Configs like rolePermissions: { admin: ["*"] }, namespace wildcards such as agents:*, or chains like admin -> editor -> viewer will be denied unless every effective permission/role is duplicated manually.

Also applies to: 477-509

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/BaseAuthProvider.ts` around lines 425 - 460, The
authorize() logic must be extended to support wildcard permission matching and
full transitive role inheritance: update getEffectiveRoles() to compute the
transitive closure of the role hierarchy (follow parent links repeatedly so
chains like admin->editor->viewer are included), update
getEffectivePermissions() to expand rolePermissions entries (treat a permission
of "*" as matching everything and preserve namespace patterns like "agents:*"),
and change the authorize() checks to consider wildcard/prefix matches (e.g.,
treat "agents:*" or "*" as satisfying "agents:read") instead of only exact set
membership; ensure missingRoles/missingPermissions are computed against these
expanded/evaluated sets so rolePermissions, namespace wildcards, and multi-hop
inheritance are honored.
test/continuous-test-suite-auth.ts-109-112 (1)

109-112: ⚠️ Potential issue | 🟠 Major

Use createRoleAuthMiddleware in the RBAC section.

Section 2 mounts createBearerAuthMiddleware() and then hand-rolls its own permission check in buildRBACApp() instead of exercising the exported RBAC middleware. This suite can pass while createRoleAuthMiddleware() regresses, because the helper imported on Line 111 is never actually used.

Also applies to: 501-617

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/continuous-test-suite-auth.ts` around lines 109 - 112, The RBAC test
suite currently mounts createBearerAuthMiddleware() but uses a hand-rolled
permission check inside buildRBACApp() instead of exercising the exported RBAC
middleware; replace the custom permission-checking logic in buildRBACApp() with
the createRoleAuthMiddleware(...) middleware (pass the same role/permission
config used by the custom check) so the tests actually invoke
createRoleAuthMiddleware and will catch regressions to that exported helper
rather than silently passing via the ad-hoc check.
src/lib/auth/providers/custom.ts-113-127 (1)

113-127: ⚠️ Potential issue | 🟠 Major

Don't hide custom session backend failures behind an in-memory fallback.

If createSessionFn throws, this code logs a warning and returns a local session anyway. That makes upstream storage/revocation failures look like success and can desynchronize session state from the caller's auth system.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/custom.ts` around lines 113 - 127, The custom session
creation block around createSessionFn should not swallow errors and proceed with
an in-memory fallback; change the behavior so that if this.createSessionFn
throws you log the error and rethrow it (or reject the caller) instead of
creating/returning a local session via this.sessions/this.userSessions and
emitting "auth:login". In practice, update the try/catch around
this.createSessionFn(user, context) to remove the fallback path and propagate
the original error (after logging) so upstream storage/revocation failures are
visible to callers.
src/lib/auth/providers/BaseAuthProvider.ts-234-388 (1)

234-388: ⚠️ Potential issue | 🟠 Major

The shared session policy is not actually applied to the new providers.

This base class centralizes customStorage, allowMultipleSessions, maxSessionsPerUser, touch(), and revocation behavior, but the new providers in this PR keep their own Map-backed create/get/refresh/destroy flows instead of delegating here. Session config will behave differently per provider until those lifecycles are routed through the base implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/BaseAuthProvider.ts` around lines 234 - 388, New
providers bypass the BaseAuthProvider session lifecycle by using their own
Map-backed create/get/refresh/destroy flows; update each provider to delegate
session operations to the base implementation (use
BaseAuthProvider.createSession, validateSession, refreshSession, revokeSession,
revokeAllSessions and this.sessionStorage) instead of maintaining internal Maps
so the centralized sessionConfig fields (allowMultipleSessions,
maxSessionsPerUser, autoRefresh/refreshThreshold, touch()) and revocation
behavior are consistently applied.
src/lib/auth/types/authTypes.ts-1-7 (1)

1-7: ⚠️ Potential issue | 🟠 Major

Make these auth contracts a single source of truth.

This new shared surface sits outside src/lib/types, and it's already drifting internally: CustomAuthConfig.validateToken/createSession are context-aware, while MastraAuthProvider.authenticateToken/createSession drop that context. Consumers typed against the interface cannot forward request metadata consistently, and sibling providers in this PR are already importing different auth type modules.

Based on learnings: Project standard: place reusable/shared types under src/lib/types/*.ts; all new type definitions outside this directory should be flagged and blocked.

Also applies to: 420-451, 712-738

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/types/authTypes.ts` around lines 1 - 7, The auth type
definitions in authTypes.ts must be moved into the project-wide types area and
unified so providers share one contract: relocate these types into src/lib/types
(e.g., types/auth.ts) and export them as the single source of truth; update the
signatures for CustomAuthConfig (validateToken, createSession) and
MastraAuthProvider.authenticateToken/createSession to accept the same
request/context parameter (the request metadata/context object used across
providers) so consumers can forward metadata consistently, then adjust all
imports to reference the new unified types file and remove the duplicate
authTypes.ts definitions.
test/continuous-test-suite-auth.ts-118-158 (1)

118-158: ⚠️ Potential issue | 🟠 Major

Tighten the SKIP: classifier.

Substrings like "not found" and "failed to" are broad enough to catch real auth regressions and rewrite them into skips. This should only skip on explicit missing-credential/connectivity signals or provider-specific error codes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/continuous-test-suite-auth.ts` around lines 118 - 158, The classifier is
too broad and converts real auth regressions into skips; update
isExpectedProviderError to only match explicit credential/connectivity signals
and provider-specific error codes (e.g., exact tokens like "api key", "api_key",
"authentication", "credentials", "cannot connect",
"ECONNREFUSED"/"econnrefused", "ENOTFOUND"/"enotfound", "403", "429", "quota",
"permission denied", "billing", "google_application_credentials", "application
default credentials", "service account", "project_id", "default credentials",
"no providers", "invalid api", "missing api", "could not resolve", "network",
"timeout") and remove overly broad substrings such as "not found" and "failed
to" (and any other generic phrases that could match unrelated errors); update
isExpectedProviderError (and consequently skipIfProviderError which uses it) to
use these tightened, explicit patterns and consider matching whole words or
uppercase variants for provider codes to avoid accidental matches.
src/lib/auth/providers/auth0.ts-89-99 (1)

89-99: ⚠️ Potential issue | 🟠 Major

Use clientId in verification or stop requiring it.

The constructor rejects configs without clientId, but token verification only uses this.audience. If callers provide the required clientId and omit audience, the value they were forced to configure never constrains verification.

🔧 Common fix
-    this.audience = config.audience;
+    this.audience = config.audience ?? config.clientId;

Also applies to: 139-142

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/auth0.ts` around lines 89 - 99, The constructor
currently enforces a required clientId but the token verification logic only
uses this.audience, so either stop requiring clientId or incorporate it into
verification; update the Auth0 provider constructor/validation and the
verifyToken (or verify) routine so that if clientId is provided/required
(this.clientId) it is passed into the Auth0/JWT verification options (as an
allowed audience or client check) or, alternatively, remove clientId from the
InvalidConfigurationError check and only require audience; make the change
consistently for both the constructor checks and the other validation block
referenced around the other occurrence (the code near the 139-142 check) so the
required fields align with actual verification behavior.
src/lib/auth/authProvider.ts-263-277 (1)

263-277: ⚠️ Potential issue | 🟠 Major

Don't persist a brand-new session on every authenticated request.

All current providers back createSession() with a stored UUID session and emit auth:login. Calling it unconditionally inside authenticateRequest() turns normal bearer-token validation into unbounded session creation, duplicate login events, and needless Redis/Map growth.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/authProvider.ts` around lines 263 - 277, authenticateRequest
currently calls createSession() for every validated token which causes unbounded
session creation and duplicate auth:login events; change authenticateRequest to
only call createSession(validation.user, context) when there is no existing
session tied to the token or request (e.g., check for a session id or flag
returned by authenticateToken like validation.sessionId or
validation.existingSession) and otherwise skip session creation and do not emit
auth:login; update authenticateToken/validation usage to surface existing
session identity if available and ensure createSession is only invoked for fresh
logins.
src/lib/auth/authContext.ts-62-124 (1)

62-124: ⚠️ Potential issue | 🟠 Major

Wire the documented fallback into the exported helpers.

globalAuthContext is presented as the non-ALS path, but every public helper here still reads only authContextStorage. In a runtime that calls globalAuthContext.set(...), getAuthContext(), getCurrentUser(), getCurrentSession(), isAuthenticated(), and requireAuth() will all still behave as if no user is present.

Also applies to: 359-360

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/authContext.ts` around lines 62 - 124, The exported helpers
(getAuthContext, getCurrentUser, getCurrentSession, isAuthenticated,
requireAuth) only read from authContextStorage and ignore the documented global
fallback; update each to first try authContextStorage.getStore() and if that is
undefined, fall back to globalAuthContext.get() (or the appropriate accessor on
globalAuthContext) so the non-ALS path is honored; ensure requireAuth uses the
fallback value before throwing and that isAuthenticated returns true when either
store or globalAuthContext has a context. Reference symbols: authContextStorage,
globalAuthContext, getAuthContext, getCurrentUser, getCurrentSession,
isAuthenticated, requireAuth.
src/lib/auth/authProvider.ts-52-59 (1)

52-59: ⚠️ Potential issue | 🟠 Major

Partial tokenExtraction configs currently erase the default header extractor.

The constructor does a shallow spread. A config like { tokenExtraction: { fromCookie: ... } } replaces the whole default object, so bearer-header auth stops working as soon as a caller adds another extraction source.

🔧 Preserve nested defaults
   constructor(config: AuthProviderConfig) {
+    const defaultTokenExtraction = {
+      fromHeader: { name: "Authorization", scheme: "Bearer" },
+    };
+
     this.config = {
-      required: true,
-      tokenExtraction: {
-        fromHeader: { name: "Authorization", scheme: "Bearer" },
-      },
-      ...config,
+      ...config,
+      required: config.required ?? true,
+      tokenExtraction: {
+        ...defaultTokenExtraction,
+        ...config.tokenExtraction,
+        fromHeader: {
+          ...defaultTokenExtraction.fromHeader,
+          ...config.tokenExtraction?.fromHeader,
+        },
+      },
     };
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/authProvider.ts` around lines 52 - 59, The constructor currently
does a shallow merge so a caller-supplied tokenExtraction object replaces the
default fromHeader extractor; update the constructor (in AuthProviderConfig
handling inside the class constructor) to deep-merge tokenExtraction instead of
overwriting it — keep the default tokenExtraction.fromHeader and merge any
provided fields like fromCookie into it (e.g., compute final tokenExtraction by
merging defaultTokenExtraction and config.tokenExtraction, then assign
this.config with other top-level spreads such as required), so adding `{
tokenExtraction: { fromCookie: ... } }` augments rather than erases the default
fromHeader.
src/lib/auth/AuthProviderFactory.ts-136-139 (1)

136-139: ⚠️ Potential issue | 🟠 Major

Initialize the factory before serving discovery data.

These discovery helpers read registrations synchronously. On a cold singleton that map is still empty until some other call runs ensureInitialized(), so provider listing and hasProvider() become order-dependent. Either make the discovery APIs async and await initialization, or register the static metadata eagerly.

Also applies to: 439-482

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/AuthProviderFactory.ts` around lines 136 - 139, The discovery
APIs (e.g., AuthProviderFactory.getAvailableProviders()) access the
registrations map synchronously on a cold singleton so listing and hasProvider()
are order-dependent; update these static discovery helpers to ensure the factory
is initialized before reading registrations: either make them async and await
factory.ensureInitialized() (call
AuthProviderFactory.getInstance().ensureInitialized()) or call a one-time eager
initializer from getInstance() so registrations are populated before returning;
apply the same change to the other static discovery methods (including
hasProvider and the helpers around lines 439-482) so all discovery paths await
or trigger ensureInitialized() before accessing registrations.
src/lib/auth/providers/jwt.ts-133-147 (1)

133-147: ⚠️ Potential issue | 🟠 Major

Add the configured algorithm allow-list to JWT verification.

this.algorithms is set during initialization but never passed to jwtVerify() in authenticateToken(). This allows any algorithm compatible with the supplied key to be accepted, bypassing the intended restriction. A provider configured as ["HS256"] can still validate tokens using other HMAC variants.

Fix
      const verifyOptions: jose.JWTVerifyOptions = {};
+     verifyOptions.algorithms = this.algorithms;

      if (this.issuer) {
        verifyOptions.issuer = this.issuer;
      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/jwt.ts` around lines 133 - 147, The JWT verification
is missing the algorithm allow-list: update authenticateToken to include
this.algorithms in the options passed to jose.jwtVerify by setting
verifyOptions.algorithms = this.algorithms (or merging into verifyOptions before
calling jwtVerify), so jwtVerify(token, this.keyObject!, verifyOptions) enforces
the configured algorithm list; reference the authenticateToken method, the
verifyOptions object, this.algorithms and the jwtVerify call.
src/lib/auth/middleware/rateLimitByUser.ts-472-475 (1)

472-475: ⚠️ Potential issue | 🟠 Major

Align this helper with createAuthMiddleware()'s actual contract.

This helper expects an auth function over Request returning { proceed, context, response? }, but createAuthMiddleware() from the sibling file resolves to a handler over AuthRequestContext with { proceed, context, error? }. The advertised composition here will not type-check or forward auth failures losslessly.

Either accept the same middleware shape as createAuthMiddleware(), or adapt Request to AuthRequestContext inside this helper before invoking auth.

Also applies to: 487-500

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/middleware/rateLimitByUser.ts` around lines 472 - 475, The
helper createAuthenticatedRateLimitMiddleware currently expects an auth function
taking a Request and returning { proceed, context, response? } but
createAuthMiddleware() actually returns a handler that accepts an
AuthRequestContext and returns { proceed, context, error? }; update
createAuthenticatedRateLimitMiddleware to either accept the same middleware
shape as createAuthMiddleware (AuthRequestContext -> { proceed, context, error?
}) or, if you want to keep a Request-based signature, adapt the Request into an
AuthRequestContext before invoking the auth handler by constructing an
AuthRequestContext object (including the original request and any required
metadata) and translate the returned { error } into the helper’s expected
response shape so auth failures are forwarded losslessly; ensure you update the
call sites and the return handling around proceed/context/error to match the
createAuthMiddleware contract (also apply the same change to the other instance
noted).
src/lib/auth/middleware/rateLimitByUser.ts-105-113 (1)

105-113: ⚠️ Potential issue | 🟠 Major

Drive bucket expiry from windowMs, not a fixed hour.

Both backends drop state after one hour regardless of the configured refill window. Any rate limit with windowMs > 3600000 will reset early after one hour of inactivity, which grants more burst capacity than configured.

Use the bucket's time-to-full / configured window when deciding the in-memory cleanup threshold and Redis TTL.

Also applies to: 126-130

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/middleware/rateLimitByUser.ts` around lines 105 - 113, The
cleanup logic currently uses a fixed one-hour cutoff; change it to compute the
expiry from the rate limit window (windowMs) and each bucket's time-to-full so
buckets are only removed after they could have fully refilled. In
cleanupExpiredBuckets(), replace the hardcoded oneHourAgo with a threshold
derived from bucket.windowMs (or the middleware-configured windowMs) and
bucket.timeToFull (or compute timeToFull = windowMs * (capacity - tokens) /
capacity) and delete entries only when lastRefill < now - max(windowMs,
timeToFull). Do the same for Redis key TTL setup (the code around the Redis
set/expire logic referenced at lines ~126-130): set Redis TTL based on windowMs
or computed time-to-full so remote state expires consistently with in-memory
buckets. Ensure you reference and use the bucket properties (lastRefill, tokens,
capacity) and the middleware's configured windowMs when computing these
thresholds.
src/lib/auth/middleware/AuthMiddleware.ts-577-585 (1)

577-585: ⚠️ Potential issue | 🟠 Major

Normalize req.url before public-route matching.

req.url often includes the query string or even an absolute URL. Using it verbatim as context.path means values like /health?ping=1 or https://host/health will not match /health in isPublicRoute(), so public endpoints can unexpectedly require auth.

Patch sketch
 export function createRequestContext(req: {
@@
 }): AuthRequestContext {
+  const rawPath = req.path ?? req.url ?? "/";
+  const path =
+    rawPath.startsWith("http://") || rawPath.startsWith("https://")
+      ? new URL(rawPath).pathname
+      : rawPath.split("?")[0].split("#")[0] || "/";
+
   return {
     method: req.method ?? "GET",
-    path: req.path ?? req.url ?? "/",
+    path,
     headers: req.headers ?? {},
     cookies: req.cookies,
     query: req.query,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/middleware/AuthMiddleware.ts` around lines 577 - 585, The
context.path assigned in AuthMiddleware (returned object where
method/path/headers are set) uses req.url verbatim which may include query
strings or absolute URLs causing isPublicRoute() mismatches; normalize req.url
by parsing it and extracting only the pathname (strip query and origin) and
prefer req.path if present, ensuring the resulting path starts with a "/" before
assigning to context.path so public-route matching works reliably.
src/lib/auth/middleware/AuthMiddleware.ts-245-280 (1)

245-280: ⚠️ Potential issue | 🟠 Major

Reject valid token results that do not include a user.

The user! / any path here turns a malformed provider response into { proceed: true } with context.user === undefined. Downstream code that only checks proceed will treat that request as authenticated, so this branch needs to fail closed when validationResult.user is missing.

Patch sketch
-      if (!validationResult.valid) {
+      if (!validationResult.valid || !validationResult.user) {
         // If auth is optional, proceed without user
         if (config.optional) {
           return { proceed: true };
         }
 
         const error = {
           statusCode: 401,
           message: validationResult.error ?? "Invalid token",
           // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
           code: (validationResult as any).errorCode ?? "AUTH-001",
         };
@@
       const authenticatedContext: AuthenticatedContext = {
         ...context,
-        user: validationResult.user! as AuthUser,
+        user: validationResult.user,
         token,
         // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
         claims: (validationResult as any).claims,
       };

As per coding guidelines, "Use strict TypeScript configuration (--strict) everywhere. Enforce no-explicit-any error in src/ (warn in test/)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/middleware/AuthMiddleware.ts` around lines 245 - 280, The code
currently trusts validationResult.valid even when validationResult.user is
missing, which can mark requests as authenticated with user undefined; change
the authenticate handling so that you require both validationResult.valid ===
true and validationResult.user to be present before creating
AuthenticatedContext: treat missing user the same as a failed validation (honor
config.optional and call config.onError if provided, and return { proceed:
false, error } with the same error shape used for other failures). Update the
branch that builds AuthenticatedContext (the spread into AuthenticatedContext,
token and claims) to only run when validationResult.user exists and remove the
unsafe any/non-null assertions (use proper types from provider.authenticateToken
result) so malformed provider responses are rejected.
🟡 Minor comments (2)
src/lib/auth/providers/BaseAuthProvider.ts-370-376 (1)

370-376: ⚠️ Potential issue | 🟡 Minor

Revocation currently collapses into “not found”.

revokeSession() marks the session invalid and deletes it immediately, so the next validateSession() returns AUTH-008 instead of AUTH-010. Keep a revoked tombstone until expiry, or remove the revoked branch from validation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/BaseAuthProvider.ts` around lines 370 - 376,
revokeSession currently sets session.isValid = false then immediately calls
this.sessionStorage.delete(sessionId), causing validateSession to treat it as
missing (AUTH-008) instead of revoked (AUTH-010); change revokeSession (in
BaseAuthProvider) to set session.isValid = false and persist it via
this.sessionStorage.save(session) but do NOT call
this.sessionStorage.delete(sessionId) so the revoked tombstone remains until
natural expiry (or alternatively update validateSession to return AUTH-010 when
session.isValid === false), ensuring validateSession observes the revoked state
rather than “not found”.
src/lib/auth/providers/supabase.ts-169-176 (1)

169-176: ⚠️ Potential issue | 🟡 Minor

Don't treat a missing email_confirmed_at field as verified.

undefined !== null is true, so payloads that omit email_confirmed_at get promoted to emailVerified: true. That can incorrectly unlock flows gated on verified email state.

🔧 Narrow fix
-      emailVerified: userData.email_confirmed_at !== null,
+      emailVerified:
+        userData.email_confirmed_at !== null &&
+        userData.email_confirmed_at !== undefined,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/supabase.ts` around lines 169 - 176, The returned user
object currently sets emailVerified using "userData.email_confirmed_at !==
null", which treats undefined as verified; change this to explicitly check for
both null and undefined (e.g., use "userData.email_confirmed_at != null" or a
boolean cast like "Boolean(userData.email_confirmed_at)") in the return block
where id/email/name/picture/roles are assembled so emailVerified is only true
when email_confirmed_at is actually present.
🧹 Nitpick comments (7)
src/lib/types/configTypes.ts (1)

107-119: Consider adding explicit union variants for all supported providers.

The union type includes auth0, clerk, firebase, supabase, workos, and better-auth, but the PR supports additional providers (jwt, oauth2, cognito, keycloak) that rely on the generic fallback { type: AuthProviderType; config: AuthProviderConfig }. While functional, adding explicit variants would improve TypeScript IntelliSense and documentation.

♻️ Suggested enhancement
 export type NeuroLinkAuthConfig =
   | MastraAuthProvider
   | { provider: MastraAuthProvider }
   | { type: "auth0"; config: Auth0Config }
   | { type: "clerk"; config: ClerkConfig }
   | { type: "firebase"; config: FirebaseConfig }
   | { type: "supabase"; config: SupabaseConfig }
   | { type: "workos"; config: WorkOSConfig }
   | { type: "better-auth"; config: BetterAuthConfig }
+  | { type: "jwt"; config: import("./authTypes.js").JWTConfig }
+  | { type: "oauth2"; config: import("./authTypes.js").OAuth2Config }
+  | { type: "cognito"; config: import("./authTypes.js").CognitoConfig }
+  | { type: "keycloak"; config: import("./authTypes.js").KeycloakConfig }
   | { type: AuthProviderType; config: AuthProviderConfig };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/types/configTypes.ts` around lines 107 - 119, NeuroLinkAuthConfig
currently lists several explicit providers but leaves jwt, oauth2, cognito, and
keycloak handled only by the generic fallback; add explicit union variants for
these providers (e.g., { type: "jwt"; config: AuthProviderConfig }, { type:
"oauth2"; config: AuthProviderConfig }, { type: "cognito"; config:
AuthProviderConfig }, { type: "keycloak"; config: AuthProviderConfig }) so
TypeScript IntelliSense and docs surface them; update the NeuroLinkAuthConfig
union to include these variants while keeping the existing fallback { type:
AuthProviderType; config: AuthProviderConfig } for any other providers.
src/cli/factories/authCommandFactory.ts (1)

102-103: Consider defining proper argument types for new command handlers.

The as any casts work but lose type safety. Since AuthCommandArgs interface exists, consider extending it or creating specific interfaces for each new command's arguments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/factories/authCommandFactory.ts` around lines 102 - 103, Replace the
unsafe cast to any when calling handleProvidersCommand by defining and using
proper argument types: create a specific interface (e.g., ProvidersCommandArgs)
that extends the existing AuthCommandArgs (or extend AuthCommandArgs with the
provider-specific fields) and update the handleProvidersCommand signature to
accept that interface instead of any; then change the call from await
handleProvidersCommand(argv as any) to await handleProvidersCommand(argv) so
TypeScript enforces the correct shape and you may adjust the CLI parsing/command
builder to produce the new typed argv where necessary.
src/lib/auth/providers/CognitoProvider.ts (4)

191-199: Consider importing JsonValue at the top of the file.

The inline import("../../types/common.js").JsonValue works but is inconsistent with the rest of the codebase. Moving it to the top-level imports improves readability.

♻️ Suggested refactor

Add to imports at top:

import type { JsonValue } from "../../types/common.js";

Then simplify:

-      const validClaims: Record<
-        string,
-        import("../../types/common.js").JsonValue
-      > = {};
+      const validClaims: Record<string, JsonValue> = {};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/CognitoProvider.ts` around lines 191 - 199, The inline
type reference import("../../types/common.js").JsonValue in CognitoProvider.ts
is inconsistent; add a top-level type import "import type { JsonValue } from
'../../types/common.js';" and then replace the inline type on validClaims with
Record<string, JsonValue>. Update occurrences in the file (e.g., the validClaims
declaration in the CognitoProvider code) to use the imported JsonValue to keep
imports consistent and improve readability.

22-27: Module-level JWKS cache limits testability.

The singleton jwksCache makes it harder to isolate tests or reset state between provider instances. Consider injecting the cache or exposing a clearCache() method for testing scenarios.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/CognitoProvider.ts` around lines 22 - 27, The
module-level singleton jwksCache (type JWKSCacheEntry) reduces testability;
modify CognitoProvider to accept an optional JWKS cache via constructor
parameter (e.g., allow passing Map<string, JWKSCacheEntry>) and fall back to the
existing jwksCache if none provided, or alternatively add and export a
clearCache() function that clears the module jwksCache; update constructor/usage
in functions that reference jwksCache to use the injected instance (or callers
to call clearCache() in tests) so tests can provide a fresh cache or reset state
between runs.

353-355: Redundant undefined check for tokenUse.

The tokenUse parameter is always a string ("id" or "access") when this method is called from authenticateToken. The undefined check is unnecessary.

-    if (tokenUse !== undefined) {
-      providerData.token_use = tokenUse;
-    }
+    providerData.token_use = tokenUse;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/CognitoProvider.ts` around lines 353 - 355, The
conditional guarding assignment to providerData.token_use is redundant because
tokenUse is always a string when called from authenticateToken; replace the if
(tokenUse !== undefined) { providerData.token_use = tokenUse; } with a direct
assignment providerData.token_use = tokenUse and, if applicable, tighten the
method signature/type for the parameter in CognitoProvider (and any callers) to
reflect a non-optional string to avoid future confusion.

379-387: Stub implementation returns null silently.

The method logs at debug level and returns null, which callers may not distinguish from "user not found." Consider documenting this limitation in the class docstring or throwing AuthProviderError.create("NOT_IMPLEMENTED", ...) if this feature is required.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/CognitoProvider.ts` around lines 379 - 387, The
getUser method in CognitoProvider currently logs a debug message and returns
null, which is ambiguous; replace the silent null return with a clear
not-implemented error by throwing AuthProviderError.create("NOT_IMPLEMENTED",
"<descriptive message>") from the getUser method (and optionally call
logger.error or logger.warn before throwing). Update the CognitoProvider class
docstring to note that getUser requires the AWS SDK if you prefer a documented
stub instead of throwing, but prefer throwing
AuthProviderError.create("NOT_IMPLEMENTED", ...) inside getUser so callers can
distinguish “not implemented” from “user not found.”
src/lib/auth/middleware/AuthMiddleware.ts (1)

11-23: Prefer $lib/@ aliases for new imports.

These deep relative imports will get brittle quickly as the auth package moves. Please use the repo TypeScript aliases here instead of introducing another relative-import island.

As per coding guidelines, "Use path aliases in TypeScript: $lib, $lib/* for SDK and @ → ./src for imports."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/middleware/AuthMiddleware.ts` around lines 11 - 23, Replace the
deep relative imports in AuthMiddleware.ts with the project's TypeScript path
aliases (e.g., $lib or @) so imports like createErrorFactory, logger,
AuthProviderFactory and the auth types (AuthErrorCode, AuthenticatedContext,
AuthMiddlewareConfig, AuthorizationResult, AuthRequestContext, AuthUser,
RBACMiddlewareConfig, TokenExtractionConfig) use the configured aliases instead
of long relative paths; update each import statement to the corresponding
alias-based path while preserving the same named imports and exported symbols so
AuthMiddleware continues to reference the same functions, classes and types.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9d16b47c-6def-4e6d-912f-890c6dd94a11

📥 Commits

Reviewing files that changed from the base of the PR and between 0c52a72 and 21834e2.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (43)
  • docs/superpowers/plans/2026-03-16-auth-providers-implementation.md
  • docs/superpowers/specs/2026-03-16-auth-providers-redesign.md
  • package.json
  • src/cli/commands/authProviders.ts
  • src/cli/factories/authCommandFactory.ts
  • src/cli/parser.ts
  • src/lib/auth/AuthProviderFactory.ts
  • src/lib/auth/AuthProviderRegistry.ts
  • src/lib/auth/RequestContext.ts
  • src/lib/auth/authContext.ts
  • src/lib/auth/authErrors.ts
  • src/lib/auth/authProvider.ts
  • src/lib/auth/index.ts
  • src/lib/auth/middleware/AuthMiddleware.ts
  • src/lib/auth/middleware/rateLimitByUser.ts
  • src/lib/auth/providers/BaseAuthProvider.ts
  • src/lib/auth/providers/CognitoProvider.ts
  • src/lib/auth/providers/KeycloakProvider.ts
  • src/lib/auth/providers/auth0.ts
  • src/lib/auth/providers/betterAuth.ts
  • src/lib/auth/providers/clerk.ts
  • src/lib/auth/providers/custom.ts
  • src/lib/auth/providers/firebase.ts
  • src/lib/auth/providers/jwt.ts
  • src/lib/auth/providers/oauth2.ts
  • src/lib/auth/providers/supabase.ts
  • src/lib/auth/providers/workos.ts
  • src/lib/auth/serverBridge.ts
  • src/lib/auth/sessionManager.ts
  • src/lib/auth/types/authTypes.ts
  • src/lib/index.ts
  • src/lib/mcp/toolRegistry.ts
  • src/lib/neurolink.ts
  • src/lib/server/routes/agentRoutes.ts
  • src/lib/types/authTypes.ts
  • src/lib/types/configTypes.ts
  • src/lib/types/generateTypes.ts
  • src/lib/types/index.ts
  • src/lib/types/streamTypes.ts
  • test/continuous-test-suite-auth.ts
  • test/fixtures/auth/provider-config.json
  • test/fixtures/auth/session-data.json
  • test/fixtures/auth/test-credentials.json
✅ Files skipped from review due to trivial changes (6)
  • package.json
  • src/cli/parser.ts
  • test/fixtures/auth/session-data.json
  • docs/superpowers/plans/2026-03-16-auth-providers-implementation.md
  • docs/superpowers/specs/2026-03-16-auth-providers-redesign.md
  • src/lib/types/authTypes.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/lib/types/streamTypes.ts
  • src/lib/auth/serverBridge.ts
  • src/lib/auth/RequestContext.ts
  • src/lib/auth/providers/firebase.ts
  • src/lib/types/index.ts
  • src/lib/auth/authErrors.ts
  • src/lib/auth/index.ts
  • src/cli/commands/authProviders.ts
  • src/lib/auth/providers/KeycloakProvider.ts

Comment thread src/lib/auth/AuthProviderRegistry.ts Outdated
Comment thread src/lib/auth/middleware/AuthMiddleware.ts
Comment thread src/lib/auth/middleware/rateLimitByUser.ts
Comment thread src/lib/auth/sessionManager.ts
@murdore
murdore force-pushed the feat/authentication-providers branch from c326783 to ae982c9 Compare March 21, 2026 11:26
@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@murdore

murdore commented Mar 22, 2026

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed (Cycle 6 — Type Consolidation + Documentation)

Type System Cleanup (BREAKING FIX)

  • Deleted src/lib/auth/types/authTypes.ts — eliminated the 890-line duplicate type file
  • Consolidated ALL auth types into src/lib/types/authTypes.ts (canonical location) — merged 15 unique types from duplicate, merged missing fields into 10+ shared types
  • Updated barrel exports in src/lib/types/index.ts — all auth types now exported from canonical location
  • Fixed all imports across 8 files that referenced the duplicate
  • Deleted docs/superpowers/ — internal design specs should not be committed

Documentation Created

  • docs/features/authentication-providers.md (906 lines) — comprehensive feature guide with Quick Start, Provider Support Table, SDK API, CLI Usage, Configuration Reference, Middleware, Session Management, Error Handling, Best Practices
  • docs/advanced/auth-architecture.md (403 lines) — architecture guide covering Factory+Registry pattern, request flow, RBAC enforcement, session lifecycle, AsyncLocalStorage context, integration points, adding new providers
  • Updated docs/features/index.md — auth providers added to Q1 2026 feature table
  • Updated docs-site/sidebars.ts — added auth docs to sidebar navigation

Validation

  • TypeScript: ✅ (0 errors)
  • ESLint: ✅ (0 errors)
  • Prettier: ✅
  • Documentation: ✅ (all links valid, Docusaurus build passes)
  • Build: ✅
  • Tests: ✅

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 22, 2026

Copy link
Copy Markdown

@murdore — Cycle 6 is an important structural cleanup. A few things worth highlighting:

  • Type deduplication — Deleting src/lib/auth/types/authTypes.ts and consolidating everything into src/lib/types/authTypes.ts is the right call. Having two 800–900 line type files covering the same domain was a maintenance hazard and a potential source of subtle drift between the two type surfaces. The canonical src/lib/types/ location is consistent with the rest of the codebase.
  • Import path fixes across 8 files — This is the necessary follow-up to the consolidation; any stale imports to the deleted file would have caused TypeScript errors at build time, so it's good these were caught and fixed as part of the same cycle.
  • docs/superpowers/ removal — Internal implementation plans and design specs shouldn't be in the committed tree; they belong in the PR description, wiki, or an internal tool. Good hygiene.
  • Documentation scope — 906 lines for the feature guide and 403 lines for the architecture guide is comprehensive coverage for a system of this complexity (12 providers, middleware, session management, RBAC, AsyncLocalStorage context propagation). This will be valuable for future contributors adding new providers.

Triggering an incremental review of the Cycle 6 changes now.


✏️ Learnings added
Learnt from: murdore
URL: https://github.com/juspay/neurolink/pull/892

Timestamp: 2026-03-22T06:45:02.738Z
Learning: In juspay/neurolink, the canonical location for auth-related TypeScript types is `src/lib/types/authTypes.ts`. The previously existing `src/lib/auth/types/authTypes.ts` was a duplicate and has been deleted in PR `#892`. All auth type imports should reference `src/lib/types/authTypes.ts` (or its barrel re-export via `src/lib/types/index.ts`).

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: juspay/neurolink PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T04:58:08.495Z
Learning: Applies to src/lib/{providers,types}/**/*.ts : No circular dependencies allowed. All providers use dynamic imports in registry, and type organization must prevent circular imports.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/auth/index.ts (1)

1-11: ⚠️ Potential issue | 🟡 Minor

Update the module header comment; it is now stale.

The header still says this module is only Anthropic OAuth + token storage, but this file now exports a full multi-provider auth system (factory/registry, middleware, session, RBAC, errors, context).

Suggested doc update
- * Provides OAuth 2.0 authentication support for Claude Pro/Max subscriptions
- * and secure token storage.
+ * Provides unified authentication support including Anthropic OAuth,
+ * pluggable auth providers, middleware (RBAC/token extraction),
+ * session management, request context, and auth utilities.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/index.ts` around lines 1 - 11, Update the stale top-of-file
header in src/lib/auth/index.ts to accurately describe the expanded
multi-provider authentication system: mention the factory/registry (authFactory,
authRegistry), middleware (authMiddleware), session management (sessionManager),
RBAC utilities (rbac), error types (AuthError), and context helpers
(AuthContext) in addition to the existing AnthropicOAuth and TokenStore; keep
the comment concise, list key components and purpose, and ensure names match the
exported symbols in this file.
♻️ Duplicate comments (10)
src/lib/neurolink.ts (1)

520-524: ⚠️ Potential issue | 🔴 Critical

Keep auth context request-scoped.

authContext still lives on the NeuroLink instance. Because this file also exports a default singleton, overlapping requests can race setAuthContext() / clearAuthContext() and leak one caller’s user/session into another. Back this with AsyncLocalStorage, like metricsTraceContextStorage, instead of an instance field.

Suggested direction
+const authContextStorage =
+  new AsyncLocalStorage<AuthenticatedContext | undefined>();
+
 export class NeuroLink {
   private authProvider?: MastraAuthProvider;
-  private authContext?: AuthenticatedContext;
   private pendingAuthConfig?: NeuroLinkAuthConfig;
   private authInitPromise?: Promise<void>;
@@
   setAuthContext(context: AuthenticatedContext): void {
-    this.authContext = context;
+    authContextStorage.enterWith(context);
     logger.debug("Auth context set", {
       userId: context.user.id,
       provider: context.provider,
       sessionId: context.session?.id,
     });
   }
@@
   getAuthContext(): AuthenticatedContext | undefined {
-    return this.authContext;
+    return authContextStorage.getStore();
   }
@@
   clearAuthContext(): void {
-    const userId = this.authContext?.user.id;
-    this.authContext = undefined;
+    const userId = authContextStorage.getStore()?.user.id;
+    authContextStorage.enterWith(undefined);
     if (userId) {
       logger.debug(`Auth context cleared for user: ${userId}`);
     }
   }

Also applies to: 11001-11025

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 520 - 524, Replace the instance-scoped
authContext field with an AsyncLocalStorage-backed store so auth is
request-scoped: introduce an AsyncLocalStorage<AuthenticatedContext | undefined>
(e.g., authContextStorage) and remove the private authContext property; update
setAuthContext(session) to run a callback via authContextStorage.run(session,
...) or authContextStorage.enterWith(session) as appropriate, update
clearAuthContext() to clear via authContextStorage.enterWith(undefined) or by
ending the run, and change any getter/uses of authContext (including in
setAuthContext, clearAuthContext, and any code referenced around
setAuthContext/clearAuthContext and auth access at symbols in this file) to read
from authContextStorage.getStore(); apply the same replacement for all
occurrences mentioned (including the range around 11001-11025) so the default
exported singleton no longer shares authContext between overlapping requests.
src/lib/auth/providers/KeycloakProvider.ts (2)

271-276: ⚠️ Potential issue | 🟠 Major

Add timeout to JWKS fetch to prevent hung requests.

The fetch() call has no timeout, so an unreachable Keycloak host will block authenticateToken() indefinitely. The CognitoProvider correctly uses AbortSignal.timeout(5000) at line 271—apply the same pattern here.

Proposed fix
     try {
-      const response = await fetch(this.jwksUri);
+      const response = await fetch(this.jwksUri, {
+        signal: AbortSignal.timeout(5000),
+      });
 
       if (!response.ok) {

As per coding guidelines: "Use withTimeout for async operations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/KeycloakProvider.ts` around lines 271 - 276, The JWKS
fetch in KeycloakProvider.authenticateToken currently uses fetch(this.jwksUri)
with no timeout; update the call to include a timeout by passing a signal (e.g.
fetch(this.jwksUri, { signal: AbortSignal.timeout(5000) })) or use the project’s
withTimeout helper for async ops, and ensure the try/catch around the fetch
handles the abort error consistently with CognitoProvider; reference the fetch
call, this.jwksUri, and authenticateToken when making the change.

138-150: ⚠️ Potential issue | 🟠 Major

Always validate aud regardless of azp presence.

The current logic only checks aud when azp exists and differs from clientId. If azp is absent—or equals clientId while aud does not contain it—the token passes without any audience verification, allowing tokens minted for another client in the same realm.

Proposed fix
       // Validate audience (azp for access tokens)
       const azp = claims.azp as string | undefined;
-      if (azp && azp !== this.keycloakConfig.clientId) {
-        // Check if clientId is in aud array
-        const audiences = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
-        if (!audiences.includes(this.keycloakConfig.clientId)) {
-          return {
-            valid: false,
-            error: `Invalid authorized party: ${azp}. Expected: ${this.keycloakConfig.clientId}`,
-            errorCode: "AUTH-001",
-          };
-        }
+      const audiences = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
+      
+      // Always verify clientId is in audiences
+      if (!audiences.includes(this.keycloakConfig.clientId)) {
+        return {
+          valid: false,
+          error: `Token audience does not include clientId: ${this.keycloakConfig.clientId}`,
+          errorCode: "AUTH-001",
+        };
+      }
+      
+      // Additionally check azp if present
+      if (azp && azp !== this.keycloakConfig.clientId) {
+        return {
+          valid: false,
+          error: `Invalid authorized party: ${azp}. Expected: ${this.keycloakConfig.clientId}`,
+          errorCode: "AUTH-001",
+        };
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/KeycloakProvider.ts` around lines 138 - 150, Always
validate the token audience regardless of azp: in KeycloakProvider (the block
referencing azp, claims.aud and this.keycloakConfig.clientId) extract audiences
= Array.isArray(claims.aud) ? claims.aud : [claims.aud] and check that
this.keycloakConfig.clientId is included; if not, return the same invalid
response (error + errorCode). Preserve the existing azp check (if azp exists and
azp !== clientId) as an additional check, but do not gate the aud validation
behind the azp conditional so tokens without azp or with azp equal to clientId
still require aud to contain the clientId.
src/lib/auth/providers/firebase.ts (2)

133-145: ⚠️ Potential issue | 🟠 Major

Add timeout to Firebase API validation call.

The fetch() call can hang indefinitely if the Google endpoint is slow or unreachable. This is on the token validation hot path.

Proposed fix
       const proxyFetch = createProxyFetch();
       const response = await proxyFetch(
         `https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=${this.apiKey}`,
         {
           method: "POST",
           headers: {
             "Content-Type": "application/json",
           },
           body: JSON.stringify({ idToken: token }),
+          signal: AbortSignal.timeout(5000),
         },
       );

As per coding guidelines: "Use withTimeout for async operations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/firebase.ts` around lines 133 - 145, The
validateViaApi method can hang because the proxyFetch call has no timeout; wrap
the async fetch call created by createProxyFetch() in the project's withTimeout
helper so the POST to
https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=${this.apiKey}
fails fast on slowness. Update the logic inside validateViaApi to call
withTimeout(proxyFetch(...), <appropriateTimeoutMs>) (and handle the timeout
error path consistently with TokenValidationResult) so token validation cannot
block indefinitely.

381-402: ⚠️ Potential issue | 🟡 Minor

Add timeout to health check fetch.

The health check is exposed via CLI and could hang indefinitely without a timeout.

Proposed fix
       const proxyFetch = createProxyFetch();
       const response = await proxyFetch(
         "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com",
+        { signal: AbortSignal.timeout(5000) },
       );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/firebase.ts` around lines 381 - 402, In the
healthCheck method, add a request timeout when calling createProxyFetch so the
health check cannot hang indefinitely: create an AbortController, set a timeout
(e.g., 5000 ms) that calls controller.abort(), pass controller.signal into the
proxyFetch call (the call site is in async healthCheck and uses
createProxyFetch()), and clear the timer after the fetch completes; ensure the
catch branch treats an AbortError like a failed fetch and returns the existing
error string logic.
src/lib/auth/providers/CognitoProvider.ts (1)

249-253: ⚠️ Potential issue | 🟠 Major

Pass clockTolerance to jwtVerify() for consistent token validation.

The configured clockTolerance is applied to the manual expiration check at line 167, but jwtVerify() is called without forwarding this tolerance. Since jwtVerify() re-validates exp/nbf/iat claims with a default tolerance of 0, a token can pass the configured tolerance window but fail signature verification with zero skew.

Proposed fix
       // Verify the JWT signature against the public key
       const publicKey = await importJWK(key, header.alg);
-      await jwtVerify(token, publicKey);
+      const clockTolerance = this.config.tokenValidation?.clockTolerance ?? 0;
+      await jwtVerify(token, publicKey, { clockTolerance });
       return true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/CognitoProvider.ts` around lines 249 - 253, jwtVerify
is called without the configured clock tolerance, causing exp/nbf/iat to be
rechecked with zero skew; update the call site where jwtVerify(token, publicKey)
is invoked (in CognitoProvider.ts near importJWK and jwtVerify) to pass the
configured clockTolerance option (the same value used in the manual expiration
check) so jwtVerify(token, publicKey, { clockTolerance }) uses a consistent
tolerance for time-based claim validation.
src/lib/auth/providers/BaseAuthProvider.ts (1)

122-136: ⚠️ Potential issue | 🟠 Major

Prune expired sessions in getForUser() to enforce maxSessionsPerUser correctly.

getForUser() returns all stored sessions including expired ones. When createSession() checks maxSessionsPerUser, it counts stale entries and may evict only one, allowing the live session count to exceed the limit.

Proposed fix
   async getForUser(userId: string): Promise<AuthSession[]> {
     const userSessionSet = this.userSessions.get(userId);
     if (!userSessionSet) {
       return [];
     }
 
     const sessions: AuthSession[] = [];
+    const expiredIds: string[] = [];
+    const now = Date.now();
+
     for (const sessionId of userSessionSet) {
       const session = this.sessions.get(sessionId);
-      if (session) {
+      if (session && session.isValid && (!session.expiresAt || session.expiresAt.getTime() > now)) {
         sessions.push(session);
+      } else if (session) {
+        expiredIds.push(sessionId);
       }
     }
+
+    // Prune expired sessions
+    for (const id of expiredIds) {
+      this.sessions.delete(id);
+      userSessionSet.delete(id);
+    }
+    if (userSessionSet.size === 0) {
+      this.userSessions.delete(userId);
+    }
+
     return sessions;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/BaseAuthProvider.ts` around lines 122 - 136,
getForUser() currently returns stored sessions including expired ones, causing
createSession() to overcount stale sessions against maxSessionsPerUser; update
getForUser() to filter out expired sessions before returning and to prune them
from the internal maps. Specifically, iterate userSessions for the given userId,
for each sessionId retrieve the AuthSession from this.sessions, check expiration
(e.g., session.expiresAt against Date.now() or session.isExpired()), and if
expired remove that sessionId from the userSessions set and delete the entry
from this.sessions; only push non-expired sessions into the returned array so
createSession() sees the correct live count.
src/lib/auth/AuthProviderFactory.ts (2)

472-482: ⚠️ Potential issue | 🟡 Minor

getAllProviderInfo() also needs initialization guard.

This synchronous method reads from registrations without ensuring the factory is initialized.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/AuthProviderFactory.ts` around lines 472 - 482,
getAllProviderInfo reads this.registrations synchronously without ensuring the
factory is initialized; add an initialization guard at the start of
getAllProviderInfo to avoid reading uninitialized state. Either throw a clear
error when !this.initialized (e.g. "AuthProviderFactory not initialized") or
convert getAllProviderInfo to async and await the existing
initialize/ensureInitialized method before accessing this.registrations;
reference getAllProviderInfo, this.registrations, and the class's
initialize/ensureInitialized/initialized members when making the change.

434-454: ⚠️ Potential issue | 🟡 Minor

Sync discovery helpers may return empty results before initialization.

getAvailableProviders(), getProviderAliases(), and getProviderMetadata() are synchronous and read from registrations directly. If called before create() triggers initialization, they return empty/undefined results. Either convert them to async with ensureInitialized() or document this limitation clearly.

Option 1: Convert to async (recommended)
-  getAvailableProviders(): string[] {
+  async getAvailableProviders(): Promise<string[]> {
+    await this.ensureInitialized();
     return Array.from(this.registrations.keys());
   }
 
-  getProviderAliases(type: string): string[] {
+  async getProviderAliases(type: string): Promise<string[]> {
+    await this.ensureInitialized();
     const registration = this.registrations.get(type.toLowerCase());
     return registration?.aliases ?? [];
   }
 
-  getProviderMetadata(
+  async getProviderMetadata(
     type: string,
-  ): AuthFactoryRegistration["metadata"] | undefined {
+  ): Promise<AuthFactoryRegistration["metadata"] | undefined> {
+    await this.ensureInitialized();
     const registration = this.registrations.get(type.toLowerCase());
     return registration?.metadata;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/AuthProviderFactory.ts` around lines 434 - 454, These
synchronous discovery helpers (getAvailableProviders, getProviderAliases,
getProviderMetadata) read this.registrations and can return empty/undefined if
called before initialization; make them call await this.ensureInitialized() and
become async (returning Promise<string[]|metadata>) so they always wait for
create() initialization (or alternatively document the limitation). Update the
method signatures to async, call await this.ensureInitialized() at the start of
each method, and keep the same return semantics (resolve to the existing
Array.from/aliases/metadata results) so callers get correct values after
initialization.
src/lib/auth/middleware/AuthMiddleware.ts (1)

250-251: ⚠️ Potential issue | 🟠 Major

Wrap authenticateToken() with a timeout to prevent request hangs.

The provider.authenticateToken(token) call can reach external JWKS/userinfo endpoints. A slow or unresponsive IdP will stall the middleware indefinitely. Per past review feedback and coding guidelines, wrap this in withTimeout to ensure deterministic failure.

Proposed fix
+import { withTimeout } from "../../utils/withTimeout.js";
+
 // ... in createAuthMiddleware handler ...
 
       // Validate token
-      const validationResult = await provider.authenticateToken(token);
+      const validationResult = await withTimeout(
+        provider.authenticateToken(token),
+        5000,
+        "Token validation timed out",
+      );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/middleware/AuthMiddleware.ts` around lines 250 - 251, Wrap the
call to provider.authenticateToken(token) in the existing withTimeout helper so
an unresponsive IdP cannot hang the middleware: replace the direct await
provider.authenticateToken(token) in AuthMiddleware with await
withTimeout(provider.authenticateToken(token), <timeoutMs>) (choose the
project-standard timeout constant or e.g. 5000), and ensure you propagate/handle
the timeout rejection the same way other authentication failures are handled so
validationResult remains falsy/error-handled on timeout.
🧹 Nitpick comments (11)
src/lib/auth/providers/auth0.ts (2)

139-142: Avoid non-null assertion on this.jwks.

Static analysis flags the ! assertion at line 139. Although initialize() is called above, a type guard would be clearer.

Proposed fix
+      if (!this.jwks) {
+        throw new ProviderInitializationError(
+          "JWKS not initialized",
+          "auth0",
+        );
+      }
-      const { payload } = await jose.jwtVerify(token, this.jwks!, {
+      const { payload } = await jose.jwtVerify(token, this.jwks, {
         issuer: `https://${this.domain}/`,
         audience: this.audience,
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/auth0.ts` around lines 139 - 142, Replace the non-null
assertion on this.jwks when calling jose.jwtVerify with an explicit type guard:
ensure this.jwks is present (e.g. call or await initialize() / check this.jwks)
and if missing throw a clear error before invoking jose.jwtVerify; reference the
jwtVerify call and the class' initialize() / this.jwks property so the guard is
added immediately prior to the jose.jwtVerify invocation.

186-193: Replace non-null assertions with fallback values.

Static analysis flags this.rolesNamespace! and this.permissionsNamespace!. These have defaults assigned in the constructor, but the ! assertion is unnecessary since you can use optional chaining with fallback.

Proposed fix
         roles:
-          (payload[this.rolesNamespace!] as string[]) ||
+          (payload[this.rolesNamespace ?? ""] as string[]) ||
           auth0Payload["https://your-namespace/roles"] ||
           [],
         permissions:
-          (payload[this.permissionsNamespace!] as string[]) ||
+          (payload[this.permissionsNamespace ?? ""] as string[]) ||
           auth0Payload["https://your-namespace/permissions"] ||
           [],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/auth0.ts` around lines 186 - 193, The code uses
non-null assertions this.rolesNamespace! and this.permissionsNamespace! when
reading payload keys; replace them with optional chaining and a fallback so
static analysis is satisfied — e.g., read payload[this.rolesNamespace ??
default] or use payload?.[this.rolesNamespace] with a fallback to the
auth0Payload keys or [] when building the roles and permissions properties in
the Auth0 provider (look for the roles/permissions object construction in
auth0.ts, inside the method that maps payload to user claims). Ensure you remove
the "!" operators and rely on optional chaining/nullish coalescing so roles and
permissions default to the provided auth0Payload keys or an empty array.
src/lib/auth/providers/clerk.ts (2)

119-119: Avoid non-null assertion on this.jwks.

Static analysis flags the ! assertion. The initialize() call above should guarantee it's set, but a guard would be cleaner.

Proposed fix
         if (!this.jwks) {
           await this.initialize();
         }
-        ({ payload } = await jose.jwtVerify(token, this.jwks!));
+        if (!this.jwks) {
+          throw new Error("Failed to initialize JWKS");
+        }
+        ({ payload } = await jose.jwtVerify(token, this.jwks));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/clerk.ts` at line 119, The code uses a non-null
assertion on this.jwks in the jose.jwtVerify call; replace the assertion with a
runtime guard: ensure initialize() has set this.jwks or throw/return a clear
error before calling jose.jwtVerify, e.g., check if this.jwks is undefined and
handle it, then call ({ payload } = await jose.jwtVerify(token, this.jwks));;
reference the initialize() method and the this.jwks property and the
jose.jwtVerify call when making the change.

78-83: Use Clerk's backend JWKS endpoint /v1/jwks instead of the public well-known endpoint.

This provider has authentication credentials (secretKey) and makes authenticated calls to Clerk's backend API. Using the backend JWKS endpoint https://api.clerk.com/v1/jwks is more consistent with the provider's design and purpose, rather than the public OpenID Connect endpoint.

Proposed fix
-    const jwksUrl = new URL("https://api.clerk.com/.well-known/jwks.json");
+    const jwksUrl = new URL("https://api.clerk.com/v1/jwks");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/clerk.ts` around lines 78 - 83, The JWKS URL used in
Clerk provider initialization is the public OIDC endpoint; update the
initialize() method to use Clerk's backend JWKS endpoint by replacing the URL
string passed to new URL(...) from "https://api.clerk.com/.well-known/jwks.json"
to "https://api.clerk.com/v1/jwks" so this.jwks =
jose.createRemoteJWKSet(jwksUrl) uses the backend key set aligned with the
provider's authenticated backend calls.
src/lib/auth/providers/jwt.ts (1)

68-69: Consider using inherited session storage instead of custom Maps.

JWTProvider maintains its own sessions and userSessions Maps, but BaseAuthProvider already provides InMemorySessionStorage (see context snippet from BaseAuthProvider.ts:79-161) with the same functionality. The custom implementation duplicates code and diverges in behavior (e.g., refreshSession returns null vs throwing SESSION_NOT_FOUND).

Consider removing the custom session Maps and relying on the inherited sessionStorage from BaseAuthProvider, or explicitly document why custom behavior is needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/jwt.ts` around lines 68 - 69, The JWTProvider
currently duplicates session management via the private fields sessions and
userSessions; instead, remove these Maps and delegate to the inherited
sessionStorage provided by BaseAuthProvider (which includes
InMemorySessionStorage) to avoid divergence. Update JWTProvider methods that
currently reference sessions/userSessions (e.g., createSession, getSession,
refreshSession, revokeSession, getUserSessions) to call the corresponding
sessionStorage methods (or adapt to its API), and ensure refreshSession
preserves the BaseAuthProvider behavior (throw SESSION_NOT_FOUND if appropriate)
or explicitly document the differing behavior if you must keep a custom
implementation. Locate usages by the class name JWTProvider and the private
fields sessions/userSessions and replace them with sessionStorage operations to
consolidate logic and remove duplication.
src/lib/auth/providers/oauth2.ts (2)

53-68: Consider using inherited sessionStorage instead of duplicating session management.

OAuth2Provider extends BaseAuthProvider but maintains separate sessions and userSessions Maps. This bypasses sessionConfig enforcement (e.g., maxSessionsPerUser, allowMultipleSessions) and creates inconsistency. Delegate to this.sessionStorage or call super.createSession() to inherit the base behavior.

Also applies to: 273-392

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/oauth2.ts` around lines 53 - 68, The OAuth2Provider
duplicates session state with private maps sessions and userSessions instead of
using the inherited session storage and lifecycle in BaseAuthProvider; remove or
stop using those maps and delegate session operations to this.sessionStorage or
call the base implementations (e.g., use super.createSession(),
super.deleteSession(), super.getSession()/super.listSessions() or
this.sessionStorage APIs) in all places that reference sessions or userSessions
(including the methods around the 273-392 region), ensuring sessionConfig rules
like maxSessionsPerUser and allowMultipleSessions are honored via the
base/sessionStorage logic.

141-173: Consider using jose.jwtVerify() options for issuer/audience validation.

The manual post-verification checks work, but jose.jwtVerify() accepts issuer and audience options that enforce these claims atomically during verification, reducing the window for logic errors.

Proposed enhancement
       try {
-        const { payload } = await jose.jwtVerify(token, this.jwks!);
-
-        // Validate issuer against the authorization server origin
-        if (payload.iss) {
-          const expectedIssuerOrigin = new URL(this.authorizationUrl).origin;
-          if (!payload.iss.startsWith(expectedIssuerOrigin)) {
-            return {
-              valid: false,
-              error: `Invalid issuer: ${payload.iss}. Expected origin: ${expectedIssuerOrigin}`,
-            };
-          }
-        }
-
-        // Validate audience against the configured clientId
-        if (payload.aud) {
-          const audiences = Array.isArray(payload.aud)
-            ? payload.aud
-            : [payload.aud];
-          if (!audiences.includes(this.clientId)) {
-            return {
-              valid: false,
-              error: `Invalid audience: ${audiences.join(", ")}. Expected: ${this.clientId}`,
-            };
-          }
-        }
+        const expectedIssuerOrigin = new URL(this.authorizationUrl).origin;
+        const { payload } = await jose.jwtVerify(token, this.jwks!, {
+          audience: this.clientId,
+          // Note: jose doesn't support prefix matching for issuer, so keep manual check if needed
+        });
+
+        // Validate issuer prefix (jose doesn't support prefix matching)
+        if (payload.iss && !payload.iss.startsWith(expectedIssuerOrigin)) {
+          return {
+            valid: false,
+            error: `Invalid issuer: ${payload.iss}. Expected origin: ${expectedIssuerOrigin}`,
+          };
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/oauth2.ts` around lines 141 - 173, Replace the manual
issuer/audience checks after calling jose.jwtVerify in the token verification
logic by passing issuer and audience options into jose.jwtVerify so verification
is atomic: compute expectedIssuerOrigin from this.authorizationUrl, call
jose.jwtVerify(token, this.jwks!, { issuer: expectedIssuerOrigin, audience:
this.clientId }), and then remove the manual payload.iss/payload.aud checks in
the try block while keeping the required payload.sub check (the 'sub' existence
check in the same function) and returning the same error if missing; ensure
errors from jwtVerify are propagated/handled the same way.
src/lib/auth/providers/custom.ts (1)

69-70: Session management duplicates BaseAuthProvider functionality.

This provider also maintains separate sessions and userSessions Maps. Consider consolidating with the base class's sessionStorage to inherit configuration enforcement and reduce duplication across providers.

Also applies to: 109-152

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/custom.ts` around lines 69 - 70, The custom provider
duplicates session storage with private maps sessions and userSessions; remove
these maps and switch all session-handling logic (methods referencing
sessions/userSessions in the Custom* provider around the blocks you saw and
lines ~109-152) to use the base class's sessionStorage instead (e.g., call
this.sessionStorage.get/set/delete or the base-provided helpers to store,
retrieve and enumerate sessions by user). Ensure you replace direct Map
operations with sessionStorage API calls, preserve existing behavior (create,
lookup by session ID, list/delete sessions for a user), and remove any remaining
references to the sessions and userSessions fields so configuration and
enforcement come from BaseAuthProvider.
src/lib/auth/providers/betterAuth.ts (1)

53-54: Session management duplicates BaseAuthProvider functionality.

Like OAuth2Provider, this class maintains its own sessions and userSessions Maps instead of using the inherited sessionStorage. This bypasses sessionConfig enforcement and creates maintenance burden. Consider delegating to the base class methods.

Also applies to: 199-277

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/betterAuth.ts` around lines 53 - 54, Remove the
duplicate in-class session storage (the private fields "sessions" and
"userSessions") and change all code that reads/writes those Maps (including the
methods between the 199-277 block) to use the inherited session storage and
helper methods on the base class instead (i.e., delegate to this.sessionStorage
and the base class session management APIs such as the base get/save/delete
session methods so sessionConfig validation/enforcement is honored). Ensure all
places that previously mutated this.sessions or this.userSessions now call the
base provider's session create/update/remove and any lookup utilities rather
than maintaining local Maps.
src/lib/auth/AuthProviderRegistry.ts (1)

344-346: Discovery methods call list() without ensuring initialization.

getAvailableTypes(), getProvidersByFeature(), and getBuiltInProviders() call this.list() which reads from items. If called before initialization, they return empty results. Consider making them async or documenting the requirement to call ensureInitialized() first.

Proposed fix
-  getAvailableTypes(): AuthProviderType[] {
+  async getAvailableTypes(): Promise<AuthProviderType[]> {
+    await this.ensureInitialized();
     return Array.from(new Set(this.list().map((p) => p.metadata.type)));
   }
 
   // ...
 
-  getProvidersByFeature(feature: string): AuthProviderMetadata[] {
+  async getProvidersByFeature(feature: string): Promise<AuthProviderMetadata[]> {
+    await this.ensureInitialized();
     return this.list()
       .filter((p) => p.metadata.features?.includes(feature))
       .map((p) => p.metadata);
   }
 
-  getBuiltInProviders(): AuthProviderMetadata[] {
+  async getBuiltInProviders(): Promise<AuthProviderMetadata[]> {
+    await this.ensureInitialized();
     return this.list()
       .filter((p) => !p.metadata.requiresExternalDependencies)
       .map((p) => p.metadata);
   }

Also applies to: 361-374

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/AuthProviderRegistry.ts` around lines 344 - 346, The discovery
methods getAvailableTypes, getProvidersByFeature, and getBuiltInProviders
currently call this.list() which reads items before the registry is initialized;
change these methods to ensure initialization first by awaiting
ensureInitialized() (or make them explicitly async and call await
this.ensureInitialized()) before calling this.list(), or add a clear doc comment
requiring callers to call ensureInitialized() first—update the signatures of
getAvailableTypes/getProvidersByFeature/getBuiltInProviders and any callers
accordingly so list() is never invoked on an uninitialized registry.
src/lib/auth/providers/BaseAuthProvider.ts (1)

765-786: Consider reusing existing sessions instead of creating new ones on every request.

authenticateRequest() calls createSession() for every valid token, which can rapidly accumulate sessions and trigger unnecessary evictions. Consider looking up an existing session by token or user first.

Sketch
   async authenticateRequest(
     context: AuthRequestContext,
   ): Promise<AuthenticatedContext | null> {
     const token = this.extractToken(context);
     if (!token) {
       return null;
     }
 
     const result = await this.authenticateToken(token);
     if (!result.valid || !result.user) {
       return null;
     }
 
-    const session = await this.createSession(result.user);
+    // Reuse existing valid session for this user if available
+    const existingSessions = await this.sessionStorage.getForUser(result.user.id);
+    let session = existingSessions.find(
+      (s) => s.isValid && s.expiresAt && s.expiresAt.getTime() > Date.now(),
+    );
+    if (!session) {
+      session = await this.createSession(result.user);
+    } else {
+      await this.sessionStorage.touch(session.id);
+    }
+
     return {
       ...context,
       user: result.user,
       session,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/BaseAuthProvider.ts` around lines 765 - 786,
authenticateRequest currently calls createSession for every validated token
which creates new sessions each request; change authenticateRequest to first
look up an existing session (by token or by user id) using the same session
store used by createSession (e.g., query by token or user in your session
repository), and if found reuse that session (optionally refresh
lastActive/authenticatedAt) instead of always calling createSession; only call
createSession when no active session exists. Use the helper methods extractToken
and authenticateToken to get token and user, then check the session store for an
existing session for that token or result.user before creating a new one.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/features/authentication-providers.md`:
- Around line 538-540: The example uses fromHeader with key "prefix" which
doesn't match the documented config shape; update the sample to use
fromHeader.scheme instead of prefix so the shared tokenExtraction config aligns
with the documented API (i.e., change tokenExtraction.fromHeader.prefix ->
tokenExtraction.fromHeader.scheme in the example). Ensure the object key is
"scheme" and the value remains "Bearer" so consumers can copy the snippet
correctly.

In `@src/lib/auth/authContext.ts`:
- Around line 143-147: The requireUser function currently throws a generic Error
on mismatch; replace that with a typed auth error so callers can special-case
auth failures. Modify requireUser (which calls requireAuth and returns
AuthenticatedContext) to throw the project’s authentication error type (e.g.,
AuthError or AuthenticationError) instead of new Error("User mismatch"), and
import that error class from the auth/errors module (or wherever auth errors
live); ensure the thrown error includes a clear message like "User mismatch" so
callers can detect and handle auth failures properly.

In `@src/lib/auth/authProvider.ts`:
- Around line 146-175: authorizeUser currently only checks user.permissions and
wildcard strings and ignores rolePermissions and superAdminRoles, while
authorizeRoles only walks one level of roleHierarchy; update both to mirror
middleware behavior by (1) in authorizeUser, expand the user's effective roles
and permissions: resolve transitive role inheritance using roleHierarchy to get
all inherited roles, include permissions from rolePermissions for every resolved
role, and treat any role in superAdminRoles as an immediate allow; (2) update
authorizeRoles to compute the full transitive closure of roleHierarchy (not just
one hop) when checking membership; ensure you reference and use the existing
symbols authorizeUser, authorizeRoles, rolePermissions, superAdminRoles,
roleHierarchy and AuthUser when implementing these changes so provider-side RBAC
matches middleware RBAC.

In `@src/lib/auth/index.ts`:
- Around line 117-135: Remove the static re-exports of concrete provider classes
(Auth0Provider, BetterAuthProvider, CognitoProvider, ClerkProvider,
CustomAuthProvider, FirebaseAuthProvider/FirebaseProvider, JWTProvider,
KeycloakProvider, OAuth2Provider, SupabaseAuthProvider/SupabaseProvider,
WorkOSProvider) from the top-level barrel and instead surface only the provider
API surface (types/interfaces) plus accessors that use a
ProviderRegistry/factory; update the module to export functions like
registerProvider, getProvider (or a ProviderRegistry instance) and ensure each
concrete provider is loaded via dynamic import inside the registry (use import()
inside ProviderRegistry’s register/get logic) so callers obtain providers
through the registry/factory rather than via direct value re-exports,
eliminating the static module edges and enabling lazy loading.

In `@src/lib/auth/middleware/AuthMiddleware.ts`:
- Around line 406-421: The non-null assertion on queue.pop() in expandRoles
should be removed and replaced with a safe guard: retrieve the popped value into
a variable (e.g., const role = queue.pop()) and check for undefined (if (!role)
continue or break) before using it; keep the rest of the logic (expanded Set,
roleHierarchy lookup, pushing children) unchanged so expandRoles and its use of
roleHierarchy remain intact.

In `@src/lib/auth/providers/betterAuth.ts`:
- Around line 219-226: The non-null assertion this.userSessions.get(user.id)!
should be replaced with a safe guard: retrieve the set into a local variable
(e.g., const sessions = this.userSessions.get(user.id)), if it's undefined
create a new Set and store it via this.userSessions.set(user.id, sessions), then
call sessions.add(sessionId); update the code around the login flow (the block
that currently creates/gets the set and calls add) so no ! is used and the map
is always populated before adding.
- Around line 101-129: In validateJWT, ensure payload.sub exists and is a string
before using it as the AuthUser.id: if payload.sub is missing or not a string
return a TokenValidationResult with valid: false and an appropriate error
message instead of constructing a user with an undefined id; update the
validateJWT logic (function validateJWT, TokenValidationResult creation, and the
constructed user object) to check typeof payload.sub === "string" and handle the
invalid case early so downstream code never receives a user with an undefined
id.

In `@src/lib/auth/providers/custom.ts`:
- Around line 119-126: The code uses a non-null assertion
this.userSessions.get(sessionUser.id)! after checking
this.userSessions.has(sessionUser.id); instead, retrieve the set into a local
variable (e.g., const sessionsForUser = this.userSessions.get(sessionUser.id))
immediately after the has() check, create and set a new Set when absent, then
call sessionsForUser.add(session.id); update the block around this.sessions.set,
this.userSessions.has, this.userSessions.get, and this.emit("auth:login",
sessionUser) to use that local reference instead of the ! assertion to make the
code null-safe.

In `@src/lib/auth/providers/firebase.ts`:
- Around line 216-239: In firebaseUserToAuthUser, guard the JSON.parse of
userData.customAttributes by wrapping it in a try-catch to avoid throwing on
malformed JSON; if parsing fails, fall back to an empty object (or a safe
default) and optionally record the parsing failure (e.g., via logger) so the
rest of the conversion (roles, permissions, metadata) can proceed safely; update
references to customAttributes.roles and customAttributes.permissions to use the
safe fallback.

In `@src/lib/auth/providers/KeycloakProvider.ts`:
- Around line 406-457: The getUser method currently awaits three fetches
(tokenResponse, userResponse, rolesResponse) with no timeouts; wrap each fetch
in an AbortController with a per-request timeout (e.g., configurable constant)
so slow Keycloak responses can't hang the call, pass controller.signal into each
fetch for tokenResponse, userResponse and rolesResponse, start a setTimeout to
call controller.abort() after the timeout, and clear the timer after the fetch
completes (or on catch) to avoid leaks; ensure aborted fetches are handled
(treat as error/timeout) and surface a clear timeout error from getUser.

In `@src/lib/auth/serverBridge.ts`:
- Around line 12-19: The validator returned by createAuthValidatorFromProvider
currently ignores the ctx param; update the returned async function to accept
(token, ctx) and pass ctx through to provider.authenticateToken so any provider
logic using request metadata (headers, IP, org hints) receives the context;
locate createAuthValidatorFromProvider and the provider.authenticateToken call
and ensure the ctx argument is forwarded.
- Around line 23-38: Remove the fabricated-user fallback: when result.user is
absent, do not construct a minimal user from result.payload; instead return null
to preserve the middleware's fail-closed behavior. Concretely, delete the branch
that checks result.payload and creates { id: (result.payload.sub as string) ??
"unknown", ... } and replace it with return null; also don't try to read
result.payload or result.claims here (KeycloakProvider/CognitoProvider use
claims), keeping the bridge consistent with the middleware's "Token valid but no
user identity resolved" rejection.

In `@src/lib/auth/sessionManager.ts`:
- Around line 432-433: The debug logs in sessionManager.ts currently emit raw
sensitive identifiers (sessionId and user.id) via logger.debug calls; update
each logger.debug that references sessionId or user.id (including the instances
around the shown line and the other occurrences flagged) to avoid printing raw
values — either remove those identifiers from the message or replace them with
non-sensitive alternatives such as a masked/truncated session token or a salted
hash/consistent pseudonymized user identifier, or log only safe metadata (e.g.,
"session_created" with userExists=true); ensure the changes are applied to all
logger.debug invocations that reference sessionId or user.id so no raw
identifiers are written to logs.
- Around line 214-228: The set(session: AuthSession) and shouldRefresh() logic
currently dereference session.expiresAt unconditionally; update both so they
first check for the presence of expiresAt and handle non-expiring/tombstoned
sessions safely: in set(), if session.expiresAt is missing, store the session
without a TTL (use client.set(...) or set with no EXPIRE) or use a safe fallback
TTL instead of calling getTime(); in shouldRefresh(), return false when
session.expiresAt is undefined (only compute time-deltas when expiresAt exists).
Apply the same guarded logic to the other occurrence referenced (lines ~463-466)
so no code calls getTime() on an undefined expiresAt.
- Around line 15-35: Remove the locally redefined SessionStorage interface and
instead consume the canonical type from the auth types module: replace the
export/interface block in sessionManager.ts with an import of SessionStorage
from src/lib/types/authTypes.ts (and re-export if this file needs to expose it),
then update any local references (e.g. SessionConfig.customStorage and
BaseAuthProvider usages) to use the imported type so there is a single shared
SessionStorage contract across the auth layer. Ensure the file no longer
declares its own SessionStorage and that the module's exports remain consistent
with the canonical type.

In `@src/lib/core/infrastructure/baseRegistry.ts`:
- Around line 28-35: The register() method currently only reads metadata from
options?.metadata, breaking callers that pass raw metadata as the fourth
positional argument; update register(id, factory, aliases = [], options?) to
accept either the new options object or legacy raw metadata: detect whether the
fourth argument is an object containing a metadata key and use options.metadata,
otherwise treat the fourth argument itself as the metadata object; then set
metadata = detectedMetadata ?? {} before calling this.items.set(id, { factory,
metadata }) so existing callers (e.g., AuthProviderFactory) keep their metadata.

In `@src/lib/neurolink.ts`:
- Around line 2997-3051: The code currently accepts authResult.valid === true
even when authResult.user is missing, allowing caller-controlled options.context
to persist; modify the auth path so that after calling
this.authProvider.authenticateToken (via withTimeout) you require
authResult.user when authResult.valid is true and throw an InvalidTokenError (or
other rejection) if user is absent, and when merging token-derived identity
fields (tokenDerivedFields) build them directly from authResult.user (not from
options.context) so token values always override requestContext; apply the same
change in the other occurrence referenced (the block around
authenticateToken/withTimeout and tokenDerivedFields at the later range).
- Around line 2999-3009: Replace raw Error throws in the auth flow with the
project's typed auth error so callers preserve auth vs non-auth semantics: when
the no-auth-provider branch in ensureAuthProvider detects a missing provider,
throw the typed AuthenticationError (or AuthError) instead of new Error(...);
likewise when calling withTimeout around authProvider.authenticateToken use an
Auth/AuthenticationError instance as the timeout error argument (or catch and
rethrow as that typed error) so the timeout path surfaces the correct auth error
type; apply the same replacement at the other occurrence around authProvider
usage (the block referenced at lines ~5579-5590) and ensure tests/type checks
still accept the typed error class.

---

Outside diff comments:
In `@src/lib/auth/index.ts`:
- Around line 1-11: Update the stale top-of-file header in src/lib/auth/index.ts
to accurately describe the expanded multi-provider authentication system:
mention the factory/registry (authFactory, authRegistry), middleware
(authMiddleware), session management (sessionManager), RBAC utilities (rbac),
error types (AuthError), and context helpers (AuthContext) in addition to the
existing AnthropicOAuth and TokenStore; keep the comment concise, list key
components and purpose, and ensure names match the exported symbols in this
file.

---

Duplicate comments:
In `@src/lib/auth/AuthProviderFactory.ts`:
- Around line 472-482: getAllProviderInfo reads this.registrations synchronously
without ensuring the factory is initialized; add an initialization guard at the
start of getAllProviderInfo to avoid reading uninitialized state. Either throw a
clear error when !this.initialized (e.g. "AuthProviderFactory not initialized")
or convert getAllProviderInfo to async and await the existing
initialize/ensureInitialized method before accessing this.registrations;
reference getAllProviderInfo, this.registrations, and the class's
initialize/ensureInitialized/initialized members when making the change.
- Around line 434-454: These synchronous discovery helpers
(getAvailableProviders, getProviderAliases, getProviderMetadata) read
this.registrations and can return empty/undefined if called before
initialization; make them call await this.ensureInitialized() and become async
(returning Promise<string[]|metadata>) so they always wait for create()
initialization (or alternatively document the limitation). Update the method
signatures to async, call await this.ensureInitialized() at the start of each
method, and keep the same return semantics (resolve to the existing
Array.from/aliases/metadata results) so callers get correct values after
initialization.

In `@src/lib/auth/middleware/AuthMiddleware.ts`:
- Around line 250-251: Wrap the call to provider.authenticateToken(token) in the
existing withTimeout helper so an unresponsive IdP cannot hang the middleware:
replace the direct await provider.authenticateToken(token) in AuthMiddleware
with await withTimeout(provider.authenticateToken(token), <timeoutMs>) (choose
the project-standard timeout constant or e.g. 5000), and ensure you
propagate/handle the timeout rejection the same way other authentication
failures are handled so validationResult remains falsy/error-handled on timeout.

In `@src/lib/auth/providers/BaseAuthProvider.ts`:
- Around line 122-136: getForUser() currently returns stored sessions including
expired ones, causing createSession() to overcount stale sessions against
maxSessionsPerUser; update getForUser() to filter out expired sessions before
returning and to prune them from the internal maps. Specifically, iterate
userSessions for the given userId, for each sessionId retrieve the AuthSession
from this.sessions, check expiration (e.g., session.expiresAt against Date.now()
or session.isExpired()), and if expired remove that sessionId from the
userSessions set and delete the entry from this.sessions; only push non-expired
sessions into the returned array so createSession() sees the correct live count.

In `@src/lib/auth/providers/CognitoProvider.ts`:
- Around line 249-253: jwtVerify is called without the configured clock
tolerance, causing exp/nbf/iat to be rechecked with zero skew; update the call
site where jwtVerify(token, publicKey) is invoked (in CognitoProvider.ts near
importJWK and jwtVerify) to pass the configured clockTolerance option (the same
value used in the manual expiration check) so jwtVerify(token, publicKey, {
clockTolerance }) uses a consistent tolerance for time-based claim validation.

In `@src/lib/auth/providers/firebase.ts`:
- Around line 133-145: The validateViaApi method can hang because the proxyFetch
call has no timeout; wrap the async fetch call created by createProxyFetch() in
the project's withTimeout helper so the POST to
https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=${this.apiKey}
fails fast on slowness. Update the logic inside validateViaApi to call
withTimeout(proxyFetch(...), <appropriateTimeoutMs>) (and handle the timeout
error path consistently with TokenValidationResult) so token validation cannot
block indefinitely.
- Around line 381-402: In the healthCheck method, add a request timeout when
calling createProxyFetch so the health check cannot hang indefinitely: create an
AbortController, set a timeout (e.g., 5000 ms) that calls controller.abort(),
pass controller.signal into the proxyFetch call (the call site is in async
healthCheck and uses createProxyFetch()), and clear the timer after the fetch
completes; ensure the catch branch treats an AbortError like a failed fetch and
returns the existing error string logic.

In `@src/lib/auth/providers/KeycloakProvider.ts`:
- Around line 271-276: The JWKS fetch in KeycloakProvider.authenticateToken
currently uses fetch(this.jwksUri) with no timeout; update the call to include a
timeout by passing a signal (e.g. fetch(this.jwksUri, { signal:
AbortSignal.timeout(5000) })) or use the project’s withTimeout helper for async
ops, and ensure the try/catch around the fetch handles the abort error
consistently with CognitoProvider; reference the fetch call, this.jwksUri, and
authenticateToken when making the change.
- Around line 138-150: Always validate the token audience regardless of azp: in
KeycloakProvider (the block referencing azp, claims.aud and
this.keycloakConfig.clientId) extract audiences = Array.isArray(claims.aud) ?
claims.aud : [claims.aud] and check that this.keycloakConfig.clientId is
included; if not, return the same invalid response (error + errorCode). Preserve
the existing azp check (if azp exists and azp !== clientId) as an additional
check, but do not gate the aud validation behind the azp conditional so tokens
without azp or with azp equal to clientId still require aud to contain the
clientId.

In `@src/lib/neurolink.ts`:
- Around line 520-524: Replace the instance-scoped authContext field with an
AsyncLocalStorage-backed store so auth is request-scoped: introduce an
AsyncLocalStorage<AuthenticatedContext | undefined> (e.g., authContextStorage)
and remove the private authContext property; update setAuthContext(session) to
run a callback via authContextStorage.run(session, ...) or
authContextStorage.enterWith(session) as appropriate, update clearAuthContext()
to clear via authContextStorage.enterWith(undefined) or by ending the run, and
change any getter/uses of authContext (including in setAuthContext,
clearAuthContext, and any code referenced around setAuthContext/clearAuthContext
and auth access at symbols in this file) to read from
authContextStorage.getStore(); apply the same replacement for all occurrences
mentioned (including the range around 11001-11025) so the default exported
singleton no longer shares authContext between overlapping requests.

---

Nitpick comments:
In `@src/lib/auth/AuthProviderRegistry.ts`:
- Around line 344-346: The discovery methods getAvailableTypes,
getProvidersByFeature, and getBuiltInProviders currently call this.list() which
reads items before the registry is initialized; change these methods to ensure
initialization first by awaiting ensureInitialized() (or make them explicitly
async and call await this.ensureInitialized()) before calling this.list(), or
add a clear doc comment requiring callers to call ensureInitialized()
first—update the signatures of
getAvailableTypes/getProvidersByFeature/getBuiltInProviders and any callers
accordingly so list() is never invoked on an uninitialized registry.

In `@src/lib/auth/providers/auth0.ts`:
- Around line 139-142: Replace the non-null assertion on this.jwks when calling
jose.jwtVerify with an explicit type guard: ensure this.jwks is present (e.g.
call or await initialize() / check this.jwks) and if missing throw a clear error
before invoking jose.jwtVerify; reference the jwtVerify call and the class'
initialize() / this.jwks property so the guard is added immediately prior to the
jose.jwtVerify invocation.
- Around line 186-193: The code uses non-null assertions this.rolesNamespace!
and this.permissionsNamespace! when reading payload keys; replace them with
optional chaining and a fallback so static analysis is satisfied — e.g., read
payload[this.rolesNamespace ?? default] or use payload?.[this.rolesNamespace]
with a fallback to the auth0Payload keys or [] when building the roles and
permissions properties in the Auth0 provider (look for the roles/permissions
object construction in auth0.ts, inside the method that maps payload to user
claims). Ensure you remove the "!" operators and rely on optional
chaining/nullish coalescing so roles and permissions default to the provided
auth0Payload keys or an empty array.

In `@src/lib/auth/providers/BaseAuthProvider.ts`:
- Around line 765-786: authenticateRequest currently calls createSession for
every validated token which creates new sessions each request; change
authenticateRequest to first look up an existing session (by token or by user
id) using the same session store used by createSession (e.g., query by token or
user in your session repository), and if found reuse that session (optionally
refresh lastActive/authenticatedAt) instead of always calling createSession;
only call createSession when no active session exists. Use the helper methods
extractToken and authenticateToken to get token and user, then check the session
store for an existing session for that token or result.user before creating a
new one.

In `@src/lib/auth/providers/betterAuth.ts`:
- Around line 53-54: Remove the duplicate in-class session storage (the private
fields "sessions" and "userSessions") and change all code that reads/writes
those Maps (including the methods between the 199-277 block) to use the
inherited session storage and helper methods on the base class instead (i.e.,
delegate to this.sessionStorage and the base class session management APIs such
as the base get/save/delete session methods so sessionConfig
validation/enforcement is honored). Ensure all places that previously mutated
this.sessions or this.userSessions now call the base provider's session
create/update/remove and any lookup utilities rather than maintaining local
Maps.

In `@src/lib/auth/providers/clerk.ts`:
- Line 119: The code uses a non-null assertion on this.jwks in the
jose.jwtVerify call; replace the assertion with a runtime guard: ensure
initialize() has set this.jwks or throw/return a clear error before calling
jose.jwtVerify, e.g., check if this.jwks is undefined and handle it, then call
({ payload } = await jose.jwtVerify(token, this.jwks));; reference the
initialize() method and the this.jwks property and the jose.jwtVerify call when
making the change.
- Around line 78-83: The JWKS URL used in Clerk provider initialization is the
public OIDC endpoint; update the initialize() method to use Clerk's backend JWKS
endpoint by replacing the URL string passed to new URL(...) from
"https://api.clerk.com/.well-known/jwks.json" to "https://api.clerk.com/v1/jwks"
so this.jwks = jose.createRemoteJWKSet(jwksUrl) uses the backend key set aligned
with the provider's authenticated backend calls.

In `@src/lib/auth/providers/custom.ts`:
- Around line 69-70: The custom provider duplicates session storage with private
maps sessions and userSessions; remove these maps and switch all
session-handling logic (methods referencing sessions/userSessions in the Custom*
provider around the blocks you saw and lines ~109-152) to use the base class's
sessionStorage instead (e.g., call this.sessionStorage.get/set/delete or the
base-provided helpers to store, retrieve and enumerate sessions by user). Ensure
you replace direct Map operations with sessionStorage API calls, preserve
existing behavior (create, lookup by session ID, list/delete sessions for a
user), and remove any remaining references to the sessions and userSessions
fields so configuration and enforcement come from BaseAuthProvider.

In `@src/lib/auth/providers/jwt.ts`:
- Around line 68-69: The JWTProvider currently duplicates session management via
the private fields sessions and userSessions; instead, remove these Maps and
delegate to the inherited sessionStorage provided by BaseAuthProvider (which
includes InMemorySessionStorage) to avoid divergence. Update JWTProvider methods
that currently reference sessions/userSessions (e.g., createSession, getSession,
refreshSession, revokeSession, getUserSessions) to call the corresponding
sessionStorage methods (or adapt to its API), and ensure refreshSession
preserves the BaseAuthProvider behavior (throw SESSION_NOT_FOUND if appropriate)
or explicitly document the differing behavior if you must keep a custom
implementation. Locate usages by the class name JWTProvider and the private
fields sessions/userSessions and replace them with sessionStorage operations to
consolidate logic and remove duplication.

In `@src/lib/auth/providers/oauth2.ts`:
- Around line 53-68: The OAuth2Provider duplicates session state with private
maps sessions and userSessions instead of using the inherited session storage
and lifecycle in BaseAuthProvider; remove or stop using those maps and delegate
session operations to this.sessionStorage or call the base implementations
(e.g., use super.createSession(), super.deleteSession(),
super.getSession()/super.listSessions() or this.sessionStorage APIs) in all
places that reference sessions or userSessions (including the methods around the
273-392 region), ensuring sessionConfig rules like maxSessionsPerUser and
allowMultipleSessions are honored via the base/sessionStorage logic.
- Around line 141-173: Replace the manual issuer/audience checks after calling
jose.jwtVerify in the token verification logic by passing issuer and audience
options into jose.jwtVerify so verification is atomic: compute
expectedIssuerOrigin from this.authorizationUrl, call jose.jwtVerify(token,
this.jwks!, { issuer: expectedIssuerOrigin, audience: this.clientId }), and then
remove the manual payload.iss/payload.aud checks in the try block while keeping
the required payload.sub check (the 'sub' existence check in the same function)
and returning the same error if missing; ensure errors from jwtVerify are
propagated/handled the same way.

Comment thread docs/features/authentication-providers.md
Comment thread src/lib/auth/authContext.ts
Comment thread src/lib/auth/authProvider.ts Outdated
Comment on lines +146 to +175
async authorizeUser(
user: AuthUser,
permission: string,
): Promise<AuthorizationResult> {
// Check if user has the permission directly
if (user.permissions.includes(permission)) {
return { authorized: true };
}

// Check if user has wildcard permission
if (user.permissions.includes("*")) {
return { authorized: true };
}

// Check permission hierarchy (e.g., "tools:*" includes "tools:execute")
const permissionParts = permission.split(":");
for (let i = permissionParts.length - 1; i > 0; i--) {
const wildcardPermission = [...permissionParts.slice(0, i), "*"].join(
":",
);
if (user.permissions.includes(wildcardPermission)) {
return { authorized: true };
}
}

return {
authorized: false,
reason: `User lacks permission: ${permission}`,
missingPermissions: [permission],
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep provider-side RBAC in sync with middleware RBAC.

authorizeUser() still ignores rolePermissions and superAdminRoles, and authorizeRoles() only walks one hop of roleHierarchy. With transitive inheritance and role-to-permission expansion now part of the auth config, these helpers can deny a user that the middleware would authorize.

Also applies to: 188-212

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/authProvider.ts` around lines 146 - 175, authorizeUser currently
only checks user.permissions and wildcard strings and ignores rolePermissions
and superAdminRoles, while authorizeRoles only walks one level of roleHierarchy;
update both to mirror middleware behavior by (1) in authorizeUser, expand the
user's effective roles and permissions: resolve transitive role inheritance
using roleHierarchy to get all inherited roles, include permissions from
rolePermissions for every resolved role, and treat any role in superAdminRoles
as an immediate allow; (2) update authorizeRoles to compute the full transitive
closure of roleHierarchy (not just one hop) when checking membership; ensure you
reference and use the existing symbols authorizeUser, authorizeRoles,
rolePermissions, superAdminRoles, roleHierarchy and AuthUser when implementing
these changes so provider-side RBAC matches middleware RBAC.

Comment thread src/lib/auth/index.ts
Comment thread src/lib/auth/middleware/AuthMiddleware.ts
Comment thread src/lib/auth/sessionManager.ts
Comment thread src/lib/auth/sessionManager.ts Outdated
Comment on lines 28 to 35
register(
id: string,
factory: () => Promise<TItem>,
metadata: TMetadata,
aliases: string[] = [],
options?: { metadata: TMetadata },
): void {
const metadata = options?.metadata ?? ({} as TMetadata);
this.items.set(id, { factory, metadata });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep register() compatible with existing metadata call sites.

register() now only reads metadata from options?.metadata, but src/lib/auth/AuthProviderFactory.ts still passes raw metadata as the fourth positional argument. That path now registers auth providers with {} metadata, so provider discovery/health listings lose their metadata until every caller is migrated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/core/infrastructure/baseRegistry.ts` around lines 28 - 35, The
register() method currently only reads metadata from options?.metadata, breaking
callers that pass raw metadata as the fourth positional argument; update
register(id, factory, aliases = [], options?) to accept either the new options
object or legacy raw metadata: detect whether the fourth argument is an object
containing a metadata key and use options.metadata, otherwise treat the fourth
argument itself as the metadata object; then set metadata = detectedMetadata ??
{} before calling this.items.set(id, { factory, metadata }) so existing callers
(e.g., AuthProviderFactory) keep their metadata.

Comment thread src/lib/neurolink.ts
Comment thread src/lib/neurolink.ts Outdated
@murdore

murdore commented Mar 22, 2026

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed (Cycle 7 — CodeRabbit Round 6)

CRITICAL Fixes (3)

  • [neurolink.ts:520] — Auth context now request-scoped — Removed instance-scoped authContext field. setAuthContext()/getAuthContext()/clearAuthContext() now delegate to AsyncLocalStorage via globalAuthContext from authContext.ts. No per-request state leaks between concurrent requests.
  • [neurolink.ts:3051] — Fail closed on valid token without user — Both generate() and stream() now throw InvalidTokenError when authenticateToken() returns valid: true but no user.
  • [serverBridge.ts:38] — Removed fabricated-user fallback — Returns null for fail-closed behavior. Forwards ctx to authenticateToken().

MAJOR Fixes (15)

  • [neurolink.ts:3009] — Typed auth errors — withTimeout and "no provider" paths now throw AuthenticationFailedError instead of generic Error
  • [AuthMiddleware.ts:250] — authenticateToken timeout — Wrapped in withTimeout(5000)
  • [KeycloakProvider.ts] — 3 fixes: JWKS fetch timeout, aud validation unconditional, getUser() 3 HTTP calls with timeouts
  • [firebase.ts] — 3 fixes: API validation timeout, health check timeout, JSON.parse try-catch
  • [CognitoProvider.ts:249] — clockTolerance forwarded to jwtVerify()
  • [BaseAuthProvider.ts:122] — getForUser() prunes expired sessions
  • [AuthProviderFactory.ts] — All discovery methods now async with ensureInitialized()
  • [authContext.ts:147] — requireUser() throws AuthenticationFailedError
  • [authProvider.ts:175] — authorizeUser() mirrors transitive RBAC (superAdminRoles, roleHierarchy, rolePermissions)
  • [sessionManager.ts] — 3 fixes: Renamed local interface to avoid shadowing, expiresAt null guards, masked sensitive IDs in logs

MINOR/NITPICK Fixes (14)

  • Non-null assertions removed in auth0, clerk, betterAuth, custom providers
  • betterAuth.ts:129 — sub claim validated before use
  • Session storage deduplicated across jwt, oauth2, custom, betterAuth (all delegate to base class)
  • clerk.ts:80 — JWKS endpoint changed to /v1/jwks
  • AuthProviderRegistry.ts — getAvailableTypes(), getProvidersByFeature(), getBuiltInProviders() now async
  • auth/index.ts — Header comment updated, concrete provider re-exports removed (lazy loading preserved)
  • docs/features/authentication-providers.md:540 — Fixed prefix → scheme
  • AuthMiddleware.ts:421 — queue.pop()! replaced with guard

Validation

  • TypeScript: ✅ (0 errors)
  • ESLint: ✅ (0 errors)
  • Prettier: ✅

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 22, 2026

Copy link
Copy Markdown

@murdore — Cycle 7 is a strong final hardening pass. A few particularly important changes worth highlighting:

  • Request-scoped auth context via AsyncLocalStorage — This is the most architecturally significant fix in this cycle. An instance-scoped authContext field on NeuroLink is fundamentally incompatible with concurrent request handling — any two simultaneous requests sharing the same NeuroLink instance would race on reads/writes to that field. Delegating to AsyncLocalStorage via globalAuthContext gives each request its own isolated context propagation chain, which is the correct model.

  • Fail-closed on valid: true without user — This closes a subtle but dangerous gap. A provider that returns valid: true with no user object could previously allow a request to proceed without any authenticated identity attached to the context. The fail-closed throw ensures this ambiguous state is never silently promoted to an authenticated request.

  • serverBridge.ts fabricated-user removal — Constructing a user from raw claims when result.user is absent was an unsafe fallback that could allow improperly shaped claims to pass through as identity. Returning null is the correct fail-closed behavior here.

  • Session storage deduplication across providers — Having jwt, oauth2, custom, and betterAuth all delegate to the base class session storage eliminates 4 parallel implementations that could drift independently over time, and centralizes the session lifecycle logic where it belongs.

  • All discovery methods in AuthProviderFactory now async with ensureInitialized() — Consistent with the hasProvider() fix from Cycle 5; prevents false negatives on any discovery call before lazy registration completes.

Triggering an incremental review of the Cycle 7 changes now.


🧠 Learnings used
Learnt from: murdore
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2026-03-22T06:45:02.738Z
Learning: In juspay/neurolink, the canonical location for auth-related TypeScript types is `src/lib/types/authTypes.ts`. The previously existing `src/lib/auth/types/authTypes.ts` was a duplicate and has been deleted in PR `#892`. All auth type imports should reference `src/lib/types/authTypes.ts` (or its barrel re-export via `src/lib/types/index.ts`).

Learnt from: RajuSudhar
Repo: juspay/neurolink PR: 227
File: src/lib/utils/redis.ts:15-15
Timestamp: 2025-11-05T20:31:04.103Z
Learning: In the juspay/neurolink repository, type centralization rules (requiring types in src/lib/types/) do not apply to private, non-exported utility types used within a single file. Simple readability helpers like `type RedisClient = ReturnType<typeof createClient>` should remain in their implementation file when they are not exported and only used locally. Only exported types shared across modules, business/domain types, and public API types require centralization.

Learnt from: CR
Repo: juspay/neurolink PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T04:58:08.495Z
Learning: Applies to src/lib/{providers,types}/**/*.ts : No circular dependencies allowed. All providers use dynamic imports in registry, and type organization must prevent circular imports.

Learnt from: RajuSudhar
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2025-11-04T22:14:18.719Z
Learning: In the juspay/neurolink repository, do not flag existing type or interface definitions located outside src/lib/types/ - these are part of a phased migration plan and will be addressed in upcoming PRs. Only enforce type centralization rules on new code going forward.

Learnt from: RajuSudhar
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2025-11-04T22:14:18.719Z
Learning: In the juspay/neurolink repository, all new type definitions must be placed in src/lib/types/. New type definitions outside this directory should be flagged and blocked in code reviews.

Learnt from: murdore
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2026-01-02T19:49:21.526Z
Learning: Project standard: Place reusable/shared types under src/lib/types/*.ts; test-only helper types under test/types/*.ts; avoid declaring local types inside source implementation files.

Learnt from: CR
Repo: juspay/neurolink PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T04:58:08.495Z
Learning: Applies to src/lib/providers/**/*.ts : Providers must support consistent core interface across all 12+ providers (OpenAI, Anthropic, Google AI Studio, AWS Bedrock, Azure, Vertex, Mistral, LiteLLM, SageMaker, Hugging Face, Ollama, OpenAI-compatible).

Learnt from: CR
Repo: juspay/neurolink PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T04:58:08.495Z
Learning: Applies to src/lib/providers/**/*.ts : All 12+ providers must support consistent core interface. When implementing new providers, ensure compatibility with existing provider patterns and test provider consistency across all providers.

Learnt from: CR
Repo: juspay/neurolink PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T04:58:08.495Z
Learning: Applies to src/lib/types/**/*.ts : All type definitions must be organized by domain (providers, generation, streaming, MCP, conversation, tools, common) in src/lib/types/ to avoid circular dependencies.

Learnt from: RajuSudhar
Repo: juspay/neurolink PR: 173
File: src/lib/index.ts:16-16
Timestamp: 2025-09-17T17:55:15.261Z
Learning: In src/lib/types/providers.ts, ProviderConfig was renamed to AIModelProviderConfig to deduplicate type names, as there was an existing ProviderConfig type that better suited the "ProviderConfig" name. This was an intentional breaking change for better type organization.

Learnt from: CR
Repo: juspay/neurolink PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T04:58:08.495Z
Learning: Applies to src/lib/neurolink.ts : Main SDK class (neurolink.ts) is large (~277KB). When modifying, maintain backward compatibility with existing API and test thoroughly.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

♻️ Duplicate comments (5)
src/lib/auth/middleware/rateLimitByUser.ts (1)

162-177: ⚠️ Potential issue | 🔴 Critical

Redis client initialization has a race condition.

Two concurrent requests calling getClient() when this.client is null will both see !this.client as true and both proceed to create separate Redis clients. This leaks connections and causes inconsistent state.

Additionally, Node Redis v5 requires registering an error listener before connect() to prevent uncaught exceptions from crashing the process.

🔧 Suggested fix using init promise
 export class RedisRateLimitStorage implements RateLimitStorage {
   private redisUrl: string;
   private prefix: string;
   private ttlSeconds: number;
   private client: RedisClient | null = null;
+  private initPromise: Promise<RedisClient> | null = null;

   // ...

   private async getClient(): Promise<RedisClient> {
-    if (!this.client) {
-      // Dynamic import to avoid loading Redis unless needed
-      const { createClient } = await import("redis");
-      const client = createClient({ url: this.redisUrl });
-      await client.connect();
-      this.client = client as unknown as RedisClient;
+    if (this.client) {
+      return this.client;
     }
-    return this.client;
+    if (!this.initPromise) {
+      this.initPromise = (async () => {
+        const { createClient } = await import("redis");
+        const client = createClient({ url: this.redisUrl });
+        client.on("error", (err) => logger.warn("Redis rate-limit client error:", err));
+        await client.connect();
+        this.client = client as unknown as RedisClient;
+        return this.client;
+      })();
+    }
+    return this.initPromise;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/middleware/rateLimitByUser.ts` around lines 162 - 177,
getClient() has a race where concurrent calls create multiple Redis clients and
it connects after creation without an error listener; make initialization atomic
by adding an init promise (e.g., this.initPromise) that is set when first
entering getClient(), have other callers await it, and only create a single
client inside that promise; also register an 'error' event handler on the client
before calling client.connect() so Node Redis v5 errors are caught; update
getClient() and constructor to use the init promise and ensure this.client is
assigned once the promise resolves.
src/lib/auth/authContext.ts (1)

374-375: ⚠️ Potential issue | 🟠 Major

The process-wide globalAuthContext singleton still poses cross-request leakage risk.

The fallback to globalAuthContext in getAuthContext(), getCurrentUser(), etc. (lines 68, 87, 98, 108-110) means that if runWithAuthContext() is never called before generate()/stream() executes, any code reading auth context will receive the last authenticated user's identity. This enables cross-request state leakage in concurrent server scenarios.

Consider either:

  1. Removing the globalAuthContext export and fallback entirely
  2. Restricting AuthContextHolder to test-only usage with clear documentation
  3. Adding runtime warnings when fallback is used in non-test environments
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/authContext.ts` around lines 374 - 375, The exported
process-wide singleton globalAuthContext in AuthContextHolder is unsafe because
getAuthContext(), getCurrentUser(), and other readers (used by generate() /
stream()) fall back to it and can leak the previous request's identity; remove
the public fallback by either deleting the globalAuthContext export and ensuring
getAuthContext() throws or returns undefined unless runWithAuthContext()
explicitly set a context, or make globalAuthContext test-only (unexported) and
gate its use behind an environment check that emits a runtime warning if used
outside tests; update callers to handle the absence of a global fallback (raise
explicit errors or require runWithAuthContext()) and adjust AuthContextHolder,
getAuthContext(), getCurrentUser(), runWithAuthContext(), generate(), and
stream() to reflect the chosen approach.
src/lib/neurolink.ts (1)

3027-3046: ⚠️ Potential issue | 🔴 Critical

Require authResult.user.id before treating the token as authenticated.

These guards only reject valid: true when user is missing entirely. A provider can still return a partial user object with no id, and both paths then merge userId: undefined into options.context while the request continues as token-authenticated. Fail closed on !authResult.user?.id before any context merge.

Also applies to: 5629-5648

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 3027 - 3046, The current auth flow treats
a token as authenticated if authResult.valid is true even when authResult.user
exists but lacks an id, allowing userId: undefined to be merged into
options.context; update the guard to fail closed by checking authResult.user?.id
(i.e., throw InvalidTokenError with the same provider type when authResult.user
is falsy or authResult.user.id is missing) before the context merge in the block
that builds options.context (refer to authResult, InvalidTokenError, and the
options.context merge), and apply the same fix at the other analogous location
handling authResult to ensure no partial user (missing id) is accepted.
src/lib/auth/authProvider.ts (2)

130-133: ⚠️ Potential issue | 🟠 Major

Await custom token extractors before handing the value to authenticateToken().

A Promise returned from strategy.custom() is truthy, so this flow skips the null check and passes the Promise object into authenticateToken() instead of a token string.

Patch sketch
-  extractToken(context: AuthRequestContext): string | null {
+  async extractToken(context: AuthRequestContext): Promise<string | null> {
@@
-      return strategy.custom(context);
+      return await strategy.custom(context);
@@
-    const token = this.extractToken(context);
+    const token = await this.extractToken(context);

Also applies to: 302-317

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/authProvider.ts` around lines 130 - 133, The custom extractor
flow is returning a Promise (from strategy.custom) directly instead of awaiting
it, causing authenticateToken to receive a Promise rather than a token string;
update the code in authProvider.ts to await the result of
strategy.custom(context) (e.g., const token = await strategy.custom(context)),
check token for null/undefined, and then return or pass that token into
authenticateToken(); apply the same change to the other custom-extractor block
referenced around the authenticateToken usage so both places await
strategy.custom before null-checking and forwarding the token.

328-334: ⚠️ Potential issue | 🟠 Major

Reuse a session only when it matches the current token/session.

Picking the first live session for validation.user.id can bind this request to another device's session id. That breaks logout, refresh, and audit flows as soon as one user holds multiple active tokens.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/authProvider.ts` around lines 328 - 334, The code currently
picks the first live session for validation.user.id which can incorrectly bind
this request to another device's session; modify the selection so validSession
is only reused when it corresponds to the current incoming session/token (e.g.,
compare session.id or session.token to the session id/token sent with the
request in context or validation), otherwise call createSession(validation.user,
context). Change the find predicate in the block that calls getUserSessions to
require both isValid/expiry and equality with the currentSessionId/currentToken
from context/validation, referencing getUserSessions, validSession,
createSession, and validation.user.id to locate and update the logic. Ensure
behavior falls back to createSession when no matching session for this
device/token exists.
🧹 Nitpick comments (6)
src/lib/auth/middleware/rateLimitByUser.ts (2)

162-166: Redis TTL should default to at least windowMs to prevent early bucket expiry.

RedisRateLimitStorage defaults ttlSeconds to 3600 (1 hour), but RateLimitConfig.windowMs could be longer. If a user configures a 24-hour rate limit window without explicitly setting redis.ttlSeconds, buckets expire after 1 hour, allowing users to bypass limits by waiting for storage expiry.

🔧 Suggested fix
-  constructor(config: { url: string; prefix?: string; ttlSeconds?: number }) {
+  constructor(config: { url: string; prefix?: string; ttlSeconds?: number; windowMs?: number }) {
     this.redisUrl = config.url;
     this.prefix = config.prefix || "neurolink:ratelimit:";
-    this.ttlSeconds = config.ttlSeconds || 3600; // 1 hour default TTL
+    // TTL must be at least as long as the rate limit window
+    const windowSeconds = config.windowMs ? Math.ceil(config.windowMs / 1000) : 3600;
+    this.ttlSeconds = config.ttlSeconds || Math.max(windowSeconds, 3600);
   }

And update createRateLimitStorage to pass windowMs through.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/middleware/rateLimitByUser.ts` around lines 162 - 166, The Redis
TTL default is too short: update RedisRateLimitStorage's constructor to default
ttlSeconds to the RateLimitConfig.windowMs (in seconds) when not provided so
buckets don't expire before the rate limit window; ensure the constructor
(symbols: RedisRateLimitStorage, ttlSeconds, redisUrl, prefix) uses the provided
ttlSeconds or falls back to Math.ceil(windowMs/1000). Also update
createRateLimitStorage to accept and pass the RateLimitConfig.windowMs (symbol:
createRateLimitStorage, windowMs) into the storage config so the storage layer
can compute the correct default TTL.

566-569: Consider masking userId in log output.

For consistency with the session manager's approach to avoiding PII in logs, consider masking the user ID here.

   async resetUser(userId: string): Promise<void> {
     await this.storage.deleteBucket(userId);
-    logger.debug(`Rate limit reset for user: ${userId}`);
+    logger.debug(`Rate limit reset for user: ${userId.slice(0, 4)}***`);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/middleware/rateLimitByUser.ts` around lines 566 - 569, The debug
log in resetUser currently prints the raw userId; change it to log a masked
identifier instead to avoid PII. Update the resetUser method (which calls
this.storage.deleteBucket and logger.debug) to compute a masked version of
userId (e.g., keep first 4 and last 2 chars and replace the middle with
asterisks, or call the existing session-manager mask helper if available) and
log that masked value instead of the plain userId while leaving the deleteBucket
call unchanged.
src/lib/auth/authContext.ts (1)

144-150: Consider using InsufficientPermissionsError for user mismatch.

Semantically, a user mismatch (authenticated as user A, requesting resource for user B) is an authorization failure rather than authentication failure. Using InsufficientPermissionsError would:

  • Enable callers to distinguish "invalid credentials" (401) from "wrong user" (403)
  • Align with REST conventions where 403 indicates "authenticated but not authorized"

However, if the intent is to avoid leaking information about which user IDs exist, AuthenticationFailedError returning 401 is also reasonable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/authContext.ts` around lines 144 - 150, The requireUser function
currently throws AuthenticationFailedError on a user-id mismatch; change it to
throw InsufficientPermissionsError to reflect an authorization (403) failure
instead of an authentication (401) failure. Update the throw in requireUser (and
add the necessary import for InsufficientPermissionsError) so when
context.user.id !== userId the code throws InsufficientPermissionsError("User
mismatch") while leaving requireAuth and the rest of requireUser behavior
unchanged.
src/lib/auth/providers/oauth2.ts (2)

139-141: Refactor to avoid non-null assertion on this.jwks.

Static analysis flags the non-null assertion. Since initialize() is called just above, this.jwks will be set, but the assertion can be avoided by checking after init.

Proposed fix
       if (!this.jwks) {
         await this.initialize();
       }
+      if (!this.jwks) {
+        return {
+          valid: false,
+          error: "Failed to initialize JWKS",
+        };
+      }

       try {
-        const { payload } = await jose.jwtVerify(token, this.jwks!);
+        const { payload } = await jose.jwtVerify(token, this.jwks);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/oauth2.ts` around lines 139 - 141, The code uses a
non-null assertion on this.jwks when calling jose.jwtVerify in the method that
follows initialize(); remove the assertion by ensuring this.jwks is present (or
initialize() succeeded) before calling jwtVerify: call await this.initialize()
if needed, check that this.jwks is truthy and throw or return a clear error if
not, then pass this.jwks to jose.jwtVerify without using the ! operator
(reference the initialize() method and the jwtVerify call that currently uses
this.jwks! to locate the change).

213-215: Refactor to avoid non-null assertion on this.userInfoUrl.

The assertion is safe here since the method is only called when this.userInfoUrl is truthy (line 196), but for clarity you can either pass the URL as a parameter or use a local check.

Proposed fix
-  private async validateViaUserInfo(
-    token: string,
-  ): Promise<TokenValidationResult> {
+  private async validateViaUserInfo(
+    token: string,
+    userInfoUrl: string,
+  ): Promise<TokenValidationResult> {
     try {
       const proxyFetch = createProxyFetch();
-      const response = await proxyFetch(this.userInfoUrl!, {
+      const response = await proxyFetch(userInfoUrl, {

And update the call site:

-      return this.validateViaUserInfo(token);
+      return this.validateViaUserInfo(token, this.userInfoUrl);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/oauth2.ts` around lines 213 - 215, The code currently
uses a non-null assertion on this.userInfoUrl when calling
createProxyFetch()/proxyFetch; change the method that calls proxyFetch (the
OAuth2 user info fetch function) to either accept a userInfoUrl: string
parameter and use that instead of this.userInfoUrl, or capture it into a local
const (e.g., const url = this.userInfoUrl) and guard with an explicit check that
throws or returns early if undefined, then pass that checked url to proxyFetch;
update all call sites to supply the URL when choosing the parameter approach and
remove the "!" non-null assertion from the proxyFetch call.
src/lib/index.ts (1)

1183-1299: Single-source the auth barrel from ./auth/index.js.

This block re-lists the auth public surface that src/lib/auth/index.ts already curates. Keeping both barrels in sync by hand is brittle; please re-export from ./auth/index.js here and only alias the small set of top-level name collisions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/index.ts` around lines 1183 - 1299, Replace the long manual
re-exports for auth with a single canonical re-export from ./auth/index.js and
only keep explicit aliases for the handful of top-level name collisions; remove
the duplicated export list (the block exporting AuthProviderFactory,
AuthProviderRegistry, BaseAuthProvider,
createAuthMiddleware/createRBACMiddleware/createProtectedMiddleware/createExpressAuthMiddleware/createRequestContext/extractToken,
UserRateLimiter/MemoryRateLimitStorage/RedisRateLimitStorage,
SessionManager/MemorySessionStorage/RedisSessionStorage,
AuthContextHolder/getCurrentUser..., AuthError/InvalidTokenError..., and the
various type exports) and instead add a single export-from for the auth barrel
and explicit alias exports for the conflicting names you rely on (examples from
the diff to preserve: export createAuthMiddleware as
createAuthProviderMiddleware, export type MiddlewareHandler as
AuthMiddlewareHandler, export type RateLimitConfig as AuthRateLimitConfig,
export type SessionManagerStorage as SessionStorageInterface), ensuring the
public surface remains identical while delegating maintenance to
./auth/index.js.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/auth/authProvider.ts`:
- Around line 16-17: There are two divergent BaseAuthProvider implementations
causing inconsistent behavior; consolidate them into the single canonical
BaseAuthProvider (the one under src/lib/auth/providers/BaseAuthProvider.ts) by
removing the duplicate file and updating all providers (auth0.ts, betterAuth.ts,
firebase.ts, jwt.ts, workos.ts, etc.) to import that canonical class; modify
BaseAuthProvider to stop hardcoding InMemorySessionStorage (add a
constructor/sessionStorage injection or factory parameter), unify the
token-extraction configuration surface and RBAC hooks, and ensure a single,
well-defined authenticateRequest() implementation (or abstract it consistently)
so all providers rely on the same core interface. Ensure exports/public path
reference the canonical BaseAuthProvider and migrate any provider-specific
overrides to extend the unified class.

In `@src/lib/auth/AuthProviderRegistry.ts`:
- Around line 363-379: getProvidersByFeature and getBuiltInProviders can return
duplicate provider metadata because BaseRegistry.register stores aliases as
separate entries and BaseRegistry.list returns them all; update each method to
deduplicate results by provider identity (e.g., provider.id or another unique
key) before mapping to metadata. Concretely: after calling this.list() and
applying the filter, reduce or convert to a Map/Set keyed by the provider's
unique identifier to remove alias duplicates, then return Array.from(...) of the
unique entries' metadata; reference getProvidersByFeature, getBuiltInProviders,
BaseRegistry.register, and BaseRegistry.list when locating the logic to change.
- Around line 90-100: The public AuthProviderRegistry.get() is a foot-gun
because the registered factory always throws a raw Error; replace this unsafe
behavior by making get() surface a typed AuthRegistryError (via your
ErrorFactory) that clearly directs callers to use createProvider() or
getMetadata(), or alternatively remove/expose get() from the public API; locate
the get method in AuthProviderRegistry and either (A) wrap its thrown error in a
new AuthRegistryError (use ErrorFactory to construct it and include actionable
text pointing to createProvider()/getMetadata()), or (B) change its visibility
to private/internal and update callers to use createProvider() and
getMetadata(); ensure any async paths use withTimeout if applicable and maintain
existing usage sites.

In `@src/lib/auth/middleware/AuthMiddleware.ts`:
- Around line 305-312: The middleware is populating AuthenticatedContext.claims
from (validationResult as any).claims which is empty because new providers put
decoded token data under payload; update the construction of
AuthenticatedContext in AuthMiddleware to use the canonical field by reading
payload (e.g. (validationResult as any).payload) or fall back to claims if
present so decoded token data is preserved; specifically change the claims
assignment in the AuthenticatedContext creation (where validationResult, user,
token are set) to pull from payload first, ensuring downstream hooks see the raw
decoded claims.

In `@src/lib/auth/providers/BaseAuthProvider.ts`:
- Around line 783-804: The authenticateRequest implementation currently mints a
new session unconditionally by calling createSession() on every authenticated
request; change it so createSession is only called when there is no existing
session associated with the validated token/user. Specifically, in
authenticateRequest (and the related authenticateToken/extractToken flow) check
for an existing session id/session object returned by authenticateToken or
present in the token payload (e.g., result.session or result.sessionId or
context.session) and only call createSession(result.user) if no existing session
is found; update authenticateToken implementations to return session/sessionId
when available so authenticateRequest can reuse that instead of always creating
a fresh session.
- Around line 122-154: The getForUser method currently only filters expired
sessions and therefore returns and counts revoked sessions; update getForUser to
also treat revoked sessions (session.isValid === false) as expired: during the
loop over userSessionSet, if session.isValid is false push its id onto
expiredIds (or a revokedIds list), skip adding it to sessions, and then in the
cleanup loop remove those ids from this.sessions and userSessionSet (same as
expired cleanup) so revoked tombstones no longer appear in getForUser() or count
toward maxSessionsPerUser; reference getForUser, revokeSession, isValid,
userSessions, and sessions when making the change.

In `@src/lib/auth/providers/clerk.ts`:
- Around line 262-266: The non-null assertion on this.userSessions.get(user.id)!
is unsafe; replace it by storing the retrieved Set in a local variable after
ensuring it exists (e.g., let sessions = this.userSessions.get(user.id); if
(!sessions) { sessions = new Set(); this.userSessions.set(user.id, sessions); }
) and then call sessions.add(sessionId); reference this.userSessions, user.id
and sessionId when making the change to remove the "!" operator and use the
local sessions variable.

In `@src/lib/auth/providers/CognitoProvider.ts`:
- Around line 166-168: The manual expiration check uses clockTolerance default 0
while the jwtVerify call defaults to 30, causing inconsistent validation; fix
this by computing a single clockTolerance variable once from
this.config.tokenValidation?.clockTolerance (use 30 as the default to match
jwtVerify) and use that same variable in the isTokenExpired call and in the
jwtVerify invocation (references: isTokenExpired, jwtVerify,
this.config.tokenValidation?.clockTolerance inside the CognitoProvider class).
- Around line 381-386: The CognitoProvider.getUser implementation currently
throws an AuthProviderError which violates the MastraAuthProvider optional
contract and BaseAuthProvider behavior; replace the throw in async
getUser(_userId: string): Promise<AuthUser | null> with a simple return null so
callers receive null when the method is not implemented (preserve the same
signature and no additional side effects).

In `@src/lib/auth/providers/firebase.ts`:
- Around line 56-57: The Firebase provider currently keeps provider-local Maps
(sessions and userSessions) and custom session lifecycle logic which bypasses
the shared session backend; remove these in-memory Maps and update all
session-related methods in firebase.ts (e.g., createSession, getSession,
updateSession, deleteSession and any session cleanup/prune logic found in the
provider between the previous Map declarations and the later lifecycle code) to
delegate to the common session storage API (use config.session.customStorage or
the shared session service used by other providers) for get/set/delete/iterate
operations, ensure you enforce shared session limits and use the common
revoke/tombstone semantics when revoking or expiring sessions, and preserve
AuthSession shape when reading/writing so behavior is identical to the other
providers.

In `@src/lib/auth/providers/jwt.ts`:
- Around line 153-170: The code currently maps missing JWT subject to an empty
string (id: payload.sub ?? ""), which allows tokens without a subject to be
treated as a valid user; update the JWT handling to reject tokens that lack a
non-empty payload.sub by validating payload.sub before constructing the AuthUser
(e.g., in the function that verifies/parses the token or immediately before
creating the user object), and if payload.sub is missing or empty return an
error/null or throw an authentication error instead of proceeding; replace the
permissive fallback in the user mapping (remove the ?? "" behavior) and ensure
any callers of the function handle the authentication failure.

In `@src/lib/auth/providers/supabase.ts`:
- Around line 229-233: The non-null assertion on this.userSessions.get(user.id)!
should be removed; instead retrieve the set into a local variable after ensuring
it exists (e.g., let sessions = this.userSessions.get(user.id); if (!sessions) {
sessions = new Set(); this.userSessions.set(user.id, sessions); } ) and then
call sessions.add(sessionId). Update the logic around this.userSessions,
user.id, and sessionId to use the local reference so no "!" is needed.
- Around line 171-175: The roles extraction is using a nonstandard claim
`payload.user_role`; update the assignment that sets `roles` to use the standard
Supabase JWT `payload.role` (e.g., `payload.role as string`) for token-type
roles and fall back to application roles from `appMetadata?.roles` for
app-specific roles, so change the ternary in the `roles:` field to check
`payload.role` not `payload.user_role` while keeping the existing fallback to
`(appMetadata?.roles as string[]) || []`; ensure `emailVerified` and
`permissions` logic remain unchanged.

In `@src/lib/auth/providers/workos.ts`:
- Around line 394-402: The catch blocks around the WorkOS user fetch currently
swallow all non-ProviderAPIError exceptions by returning null; change them so
only an explicit "not found" response maps to null: if the caught error is a
ProviderAPIError (or equivalent WorkOS API error) and indicates a 404 /
user-not-found status or the API returned an empty result, return null,
otherwise rethrow the error after logging so transport/retry/outage failures
surface to callers; update both catch sites used when fetching WorkOS users (the
catch shown and the similar one later) to implement this conditional rethrow
logic while keeping the existing error log.

In `@src/lib/auth/sessionManager.ts`:
- Around line 210-218: The code calls new Date(session.expiresAt)
unconditionally which yields an Invalid Date when expiresAt is missing; update
the session parsing logic (where session.createdAt and session.expiresAt are
handled in SessionManager) to only parse expiresAt if session.expiresAt !=
null/undefined and results in a valid date (check !isNaN(parsed.getTime())); if
expiresAt is absent or invalid leave session.expiresAt undefined/null (or skip
the expiration check) so the if (new Date() > session.expiresAt) branch (which
may call this.delete(sessionId)) only runs when expiresAt is a valid Date.

In `@src/lib/neurolink.ts`:
- Around line 10980-11003: The lazy auth init currently leaves authInitPromise
set and can race with manual overrides in setAuthProvider; change
ensureAuthProvider and setAuthProvider to track and invalidate the init promise
on settle and ignore stale completions: when starting a lazy init create a local
initId/marker and assign authInitPromise, then in the async completion check
whether the current authInitPromise or a stored marker still matches (or whether
this.authProvider was set meanwhile) before assigning this.authProvider; also
add a finally block to always clear/authInitPromise = undefined on settle so
failed inits don't poison future calls; apply the same pattern to any other
lazy-init paths referenced around ensureAuthProvider/authInitPromise to make
late resolutions no-ops.
- Around line 11047-11075: The methods setAuthContext, getAuthContext, and
clearAuthContext are using require("./auth/authContext.js") which fails under
ESM; replace each require(...) call with a dynamic import(...) and destructure
the exported members from the resolved module (e.g., const { globalAuthContext }
= await import("./auth/authContext.js") in setAuthContext/clearAuthContext, and
const { getAuthContext: getCtx } = await import("./auth/authContext.js") in
getAuthContext), ensuring the functions remain synchronous in signature but
await the import inside the method or make them async if needed to await the
dynamic import; update usages of globalAuthContext and getCtx accordingly.

---

Duplicate comments:
In `@src/lib/auth/authContext.ts`:
- Around line 374-375: The exported process-wide singleton globalAuthContext in
AuthContextHolder is unsafe because getAuthContext(), getCurrentUser(), and
other readers (used by generate() / stream()) fall back to it and can leak the
previous request's identity; remove the public fallback by either deleting the
globalAuthContext export and ensuring getAuthContext() throws or returns
undefined unless runWithAuthContext() explicitly set a context, or make
globalAuthContext test-only (unexported) and gate its use behind an environment
check that emits a runtime warning if used outside tests; update callers to
handle the absence of a global fallback (raise explicit errors or require
runWithAuthContext()) and adjust AuthContextHolder, getAuthContext(),
getCurrentUser(), runWithAuthContext(), generate(), and stream() to reflect the
chosen approach.

In `@src/lib/auth/authProvider.ts`:
- Around line 130-133: The custom extractor flow is returning a Promise (from
strategy.custom) directly instead of awaiting it, causing authenticateToken to
receive a Promise rather than a token string; update the code in authProvider.ts
to await the result of strategy.custom(context) (e.g., const token = await
strategy.custom(context)), check token for null/undefined, and then return or
pass that token into authenticateToken(); apply the same change to the other
custom-extractor block referenced around the authenticateToken usage so both
places await strategy.custom before null-checking and forwarding the token.
- Around line 328-334: The code currently picks the first live session for
validation.user.id which can incorrectly bind this request to another device's
session; modify the selection so validSession is only reused when it corresponds
to the current incoming session/token (e.g., compare session.id or session.token
to the session id/token sent with the request in context or validation),
otherwise call createSession(validation.user, context). Change the find
predicate in the block that calls getUserSessions to require both isValid/expiry
and equality with the currentSessionId/currentToken from context/validation,
referencing getUserSessions, validSession, createSession, and validation.user.id
to locate and update the logic. Ensure behavior falls back to createSession when
no matching session for this device/token exists.

In `@src/lib/auth/middleware/rateLimitByUser.ts`:
- Around line 162-177: getClient() has a race where concurrent calls create
multiple Redis clients and it connects after creation without an error listener;
make initialization atomic by adding an init promise (e.g., this.initPromise)
that is set when first entering getClient(), have other callers await it, and
only create a single client inside that promise; also register an 'error' event
handler on the client before calling client.connect() so Node Redis v5 errors
are caught; update getClient() and constructor to use the init promise and
ensure this.client is assigned once the promise resolves.

In `@src/lib/neurolink.ts`:
- Around line 3027-3046: The current auth flow treats a token as authenticated
if authResult.valid is true even when authResult.user exists but lacks an id,
allowing userId: undefined to be merged into options.context; update the guard
to fail closed by checking authResult.user?.id (i.e., throw InvalidTokenError
with the same provider type when authResult.user is falsy or authResult.user.id
is missing) before the context merge in the block that builds options.context
(refer to authResult, InvalidTokenError, and the options.context merge), and
apply the same fix at the other analogous location handling authResult to ensure
no partial user (missing id) is accepted.

---

Nitpick comments:
In `@src/lib/auth/authContext.ts`:
- Around line 144-150: The requireUser function currently throws
AuthenticationFailedError on a user-id mismatch; change it to throw
InsufficientPermissionsError to reflect an authorization (403) failure instead
of an authentication (401) failure. Update the throw in requireUser (and add the
necessary import for InsufficientPermissionsError) so when context.user.id !==
userId the code throws InsufficientPermissionsError("User mismatch") while
leaving requireAuth and the rest of requireUser behavior unchanged.

In `@src/lib/auth/middleware/rateLimitByUser.ts`:
- Around line 162-166: The Redis TTL default is too short: update
RedisRateLimitStorage's constructor to default ttlSeconds to the
RateLimitConfig.windowMs (in seconds) when not provided so buckets don't expire
before the rate limit window; ensure the constructor (symbols:
RedisRateLimitStorage, ttlSeconds, redisUrl, prefix) uses the provided
ttlSeconds or falls back to Math.ceil(windowMs/1000). Also update
createRateLimitStorage to accept and pass the RateLimitConfig.windowMs (symbol:
createRateLimitStorage, windowMs) into the storage config so the storage layer
can compute the correct default TTL.
- Around line 566-569: The debug log in resetUser currently prints the raw
userId; change it to log a masked identifier instead to avoid PII. Update the
resetUser method (which calls this.storage.deleteBucket and logger.debug) to
compute a masked version of userId (e.g., keep first 4 and last 2 chars and
replace the middle with asterisks, or call the existing session-manager mask
helper if available) and log that masked value instead of the plain userId while
leaving the deleteBucket call unchanged.

In `@src/lib/auth/providers/oauth2.ts`:
- Around line 139-141: The code uses a non-null assertion on this.jwks when
calling jose.jwtVerify in the method that follows initialize(); remove the
assertion by ensuring this.jwks is present (or initialize() succeeded) before
calling jwtVerify: call await this.initialize() if needed, check that this.jwks
is truthy and throw or return a clear error if not, then pass this.jwks to
jose.jwtVerify without using the ! operator (reference the initialize() method
and the jwtVerify call that currently uses this.jwks! to locate the change).
- Around line 213-215: The code currently uses a non-null assertion on
this.userInfoUrl when calling createProxyFetch()/proxyFetch; change the method
that calls proxyFetch (the OAuth2 user info fetch function) to either accept a
userInfoUrl: string parameter and use that instead of this.userInfoUrl, or
capture it into a local const (e.g., const url = this.userInfoUrl) and guard
with an explicit check that throws or returns early if undefined, then pass that
checked url to proxyFetch; update all call sites to supply the URL when choosing
the parameter approach and remove the "!" non-null assertion from the proxyFetch
call.

In `@src/lib/index.ts`:
- Around line 1183-1299: Replace the long manual re-exports for auth with a
single canonical re-export from ./auth/index.js and only keep explicit aliases
for the handful of top-level name collisions; remove the duplicated export list
(the block exporting AuthProviderFactory, AuthProviderRegistry,
BaseAuthProvider,
createAuthMiddleware/createRBACMiddleware/createProtectedMiddleware/createExpressAuthMiddleware/createRequestContext/extractToken,
UserRateLimiter/MemoryRateLimitStorage/RedisRateLimitStorage,
SessionManager/MemorySessionStorage/RedisSessionStorage,
AuthContextHolder/getCurrentUser..., AuthError/InvalidTokenError..., and the
various type exports) and instead add a single export-from for the auth barrel
and explicit alias exports for the conflicting names you rely on (examples from
the diff to preserve: export createAuthMiddleware as
createAuthProviderMiddleware, export type MiddlewareHandler as
AuthMiddlewareHandler, export type RateLimitConfig as AuthRateLimitConfig,
export type SessionManagerStorage as SessionStorageInterface), ensuring the
public surface remains identical while delegating maintenance to
./auth/index.js.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3b47a217-c653-4690-b66a-95dba3ad3fa0

📥 Commits

Reviewing files that changed from the base of the PR and between 75c93a4 and 0bfef96.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (49)
  • docs-site/sidebars.ts
  • docs/advanced/auth-architecture.md
  • docs/features/authentication-providers.md
  • docs/features/index.md
  • docs/superpowers/plans/2026-03-16-remove-redundant-tests.md
  • package.json
  • src/cli/commands/authProviders.ts
  • src/cli/factories/authCommandFactory.ts
  • src/cli/parser.ts
  • src/lib/auth/AuthProviderFactory.ts
  • src/lib/auth/AuthProviderRegistry.ts
  • src/lib/auth/RequestContext.ts
  • src/lib/auth/authContext.ts
  • src/lib/auth/authErrors.ts
  • src/lib/auth/authProvider.ts
  • src/lib/auth/index.ts
  • src/lib/auth/middleware/AuthMiddleware.ts
  • src/lib/auth/middleware/rateLimitByUser.ts
  • src/lib/auth/providers/BaseAuthProvider.ts
  • src/lib/auth/providers/CognitoProvider.ts
  • src/lib/auth/providers/KeycloakProvider.ts
  • src/lib/auth/providers/auth0.ts
  • src/lib/auth/providers/betterAuth.ts
  • src/lib/auth/providers/clerk.ts
  • src/lib/auth/providers/custom.ts
  • src/lib/auth/providers/firebase.ts
  • src/lib/auth/providers/jwt.ts
  • src/lib/auth/providers/oauth2.ts
  • src/lib/auth/providers/supabase.ts
  • src/lib/auth/providers/workos.ts
  • src/lib/auth/serverBridge.ts
  • src/lib/auth/sessionManager.ts
  • src/lib/core/infrastructure/baseRegistry.ts
  • src/lib/index.ts
  • src/lib/mcp/toolRegistry.ts
  • src/lib/neurolink.ts
  • src/lib/rag/ChunkerRegistry.ts
  • src/lib/rag/metadata/MetadataExtractorRegistry.ts
  • src/lib/rag/reranker/RerankerRegistry.ts
  • src/lib/server/routes/agentRoutes.ts
  • src/lib/types/authTypes.ts
  • src/lib/types/configTypes.ts
  • src/lib/types/generateTypes.ts
  • src/lib/types/index.ts
  • src/lib/types/streamTypes.ts
  • test/continuous-test-suite-auth.ts
  • test/fixtures/auth/provider-config.json
  • test/fixtures/auth/session-data.json
  • test/fixtures/auth/test-credentials.json
💤 Files with no reviewable changes (1)
  • docs/superpowers/plans/2026-03-16-remove-redundant-tests.md
✅ Files skipped from review due to trivial changes (11)
  • docs-site/sidebars.ts
  • src/cli/parser.ts
  • src/lib/rag/metadata/MetadataExtractorRegistry.ts
  • test/fixtures/auth/session-data.json
  • test/fixtures/auth/provider-config.json
  • src/lib/auth/RequestContext.ts
  • test/fixtures/auth/test-credentials.json
  • src/cli/factories/authCommandFactory.ts
  • src/lib/auth/AuthProviderFactory.ts
  • src/lib/types/authTypes.ts
  • src/lib/auth/authErrors.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • package.json
  • src/lib/rag/reranker/RerankerRegistry.ts
  • src/lib/rag/ChunkerRegistry.ts
  • src/lib/mcp/toolRegistry.ts
  • src/lib/core/infrastructure/baseRegistry.ts
  • src/lib/types/streamTypes.ts
  • docs/features/index.md
  • test/continuous-test-suite-auth.ts
  • src/lib/types/configTypes.ts
  • src/cli/commands/authProviders.ts

Comment thread src/lib/auth/authProvider.ts Outdated
Comment thread src/lib/auth/AuthProviderRegistry.ts Outdated
Comment on lines +90 to +100
* @example
* ```typescript
* // Get registry instance
* const registry = AuthProviderRegistry.getInstance();
*
* // List available providers
* const providers = registry.list();
*
* // Get provider info
* const auth0Info = await registry.get('auth0');
*

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't leave get() as a public foot-gun.

The example on Line 99 advertises registry.get("auth0"), but the factory registered here always throws, and it throws a raw Error. So AuthProviderRegistry exposes a public method that fails by design and bypasses the typed auth error surface. Please either override get() with an AuthRegistryError that points callers to createProvider() / getMetadata(), or stop exposing the value-registry API here.

Suggested fix
+  override async get(_id: string): Promise<MastraAuthProvider | undefined> {
+    throw AuthRegistryError.create(
+      "REGISTRATION_FAILED",
+      "AuthProviderRegistry.get() is unsupported. Use createProvider(type, config) or getMetadata(type).",
+    );
+  }
+
   private registerProviderMetadata(
     type: string,
     metadata: AuthProviderMetadata,
   ): void {
     // Register with base registry — aliases as 3rd arg, metadata in options object as 4th arg
     this.register(
       type,
       async () => {
-        throw new Error(
-          `Direct provider creation not supported. Use AuthProviderFactory.create() with config.`,
-        );
+        throw AuthRegistryError.create(
+          "REGISTRATION_FAILED",
+          "Direct provider creation is unsupported. Use createProvider(type, config).",
+        );
       },
       metadata.aliases,
       { metadata },
     );
   }

As per coding guidelines, "Use ErrorFactory for typed errors, withTimeout for async operations, and graceful degradation with provider fallback for error handling."

Also applies to: 295-309

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/AuthProviderRegistry.ts` around lines 90 - 100, The public
AuthProviderRegistry.get() is a foot-gun because the registered factory always
throws a raw Error; replace this unsafe behavior by making get() surface a typed
AuthRegistryError (via your ErrorFactory) that clearly directs callers to use
createProvider() or getMetadata(), or alternatively remove/expose get() from the
public API; locate the get method in AuthProviderRegistry and either (A) wrap
its thrown error in a new AuthRegistryError (use ErrorFactory to construct it
and include actionable text pointing to createProvider()/getMetadata()), or (B)
change its visibility to private/internal and update callers to use
createProvider() and getMetadata(); ensure any async paths use withTimeout if
applicable and maintain existing usage sites.

Comment thread src/lib/auth/AuthProviderRegistry.ts Outdated
Comment thread src/lib/auth/middleware/AuthMiddleware.ts
Comment thread src/lib/auth/providers/BaseAuthProvider.ts
Comment thread src/lib/auth/providers/supabase.ts Outdated
Comment thread src/lib/auth/providers/workos.ts Outdated
Comment on lines +210 to +218
// Parse dates
session.createdAt = new Date(session.createdAt);
session.expiresAt = new Date(session.expiresAt);

// Check expiration
if (new Date() > session.expiresAt) {
await this.delete(sessionId);
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard expiresAt parsing for sessions that may lack expiration.

If session.expiresAt is missing from the stored JSON (e.g., tombstoned or externally created sessions), new Date(undefined) produces an Invalid Date, which may cause unexpected comparison behavior on line 215.

🛡️ Suggested defensive guard
       // Parse dates
       session.createdAt = new Date(session.createdAt);
-      session.expiresAt = new Date(session.expiresAt);
+      if (session.expiresAt) {
+        session.expiresAt = new Date(session.expiresAt);
+      }

       // Check expiration
-      if (new Date() > session.expiresAt) {
+      if (session.expiresAt && new Date() > session.expiresAt) {
         await this.delete(sessionId);
         return null;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/sessionManager.ts` around lines 210 - 218, The code calls new
Date(session.expiresAt) unconditionally which yields an Invalid Date when
expiresAt is missing; update the session parsing logic (where session.createdAt
and session.expiresAt are handled in SessionManager) to only parse expiresAt if
session.expiresAt != null/undefined and results in a valid date (check
!isNaN(parsed.getTime())); if expiresAt is absent or invalid leave
session.expiresAt undefined/null (or skip the expiration check) so the if (new
Date() > session.expiresAt) branch (which may call this.delete(sessionId)) only
runs when expiresAt is a valid Date.

Comment thread src/lib/neurolink.ts
Comment thread src/lib/neurolink.ts Outdated
@murdore

murdore commented Mar 22, 2026

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed (Cycle 8 — CodeRabbit Round 7 + Vercel Fix)

Vercel Fix

  • Regenerated landing/pnpm-lock.yaml — Root cause: commit 86b0a326 added pnpm.overrides to landing/package.json but never regenerated the lockfile. Vercel's pnpm install --frozen-lockfile detected the mismatch.

CRITICAL Fixes (2)

  • [neurolink.ts:3027] — Fail closed on missing user.id — Both generate/stream now throw InvalidTokenError when authResult.user has no .id
  • [rateLimitByUser.ts:162] — Redis client init race — Added initPromise singleton pattern to prevent duplicate connections

MAJOR Fixes (13)

  • [neurolink.ts:11047] — require() in ESM — Converted setAuthContext/getAuthContext/clearAuthContext from sync+require to async+import
  • [neurolink.ts:10980] — authInitPromise poisoning — Cleared on rejection + on setAuthProvider() call
  • [neurolink.ts:3009] — Typed auth errors — withTimeout and "no provider" paths now throw AuthenticationFailedError
  • [AuthMiddleware.ts:305] — claims vs payload — Reads payload first, falls back to claims
  • [AuthMiddleware.ts:250] — authenticateToken timeout — Wrapped in withTimeout(5000)
  • [authProvider.ts:130] — Async custom extractor — extractToken() now async, handles Promise-returning custom extractors
  • [authProvider.ts:175] — Full transitive RBAC — authorizeUser() mirrors middleware: superAdminRoles, roleHierarchy BFS, rolePermissions
  • [BaseAuthProvider.ts:122] — getForUser() prunes revoked — Filters isValid === false in addition to expired
  • [BaseAuthProvider.ts:783] — authenticateRequest session reuse — Checks existing sessions before minting
  • [CognitoProvider.ts:381] — getUser() returns null — Was throwing, violating MastraAuthProvider contract
  • [firebase.ts:56] — Session storage delegated — Removed local Maps, uses base class sessionStorage
  • [jwt.ts:153] — Reject empty sub — Tokens without non-empty payload.sub now rejected
  • [workos.ts:394] — Rethrow transport errors — Only 404 returns null; actual errors propagated

MINOR/NITPICK Fixes (15)

  • Non-null assertions fixed in clerk, supabase, oauth2
  • supabase.ts:171 — payload.user_role → payload.role
  • sessionManager.ts:210 — Guard against new Date(undefined)
  • AuthProviderRegistry.ts — get() throws typed error, deduplicates aliases
  • CognitoProvider.ts:166 — clockTolerance default 0→30
  • rateLimitByUser.ts — Redis TTL respects windowMs, userId masked in logs
  • authContext.ts:144 — User mismatch throws 403 not 401
  • index.ts — Auth barrel re-exports consolidated to single source
  • KeycloakProvider.ts — aud validation unconditional, getUser() timeouts, JWKS timeout
  • firebase.ts — JSON.parse try-catch, health check timeout

Validation

  • TypeScript: ✅ (0 errors in auth code)
  • ESLint: ✅ (0 errors)
  • Prettier: ✅

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 22, 2026

Copy link
Copy Markdown

@murdore — Cycle 8 is a thorough final hardening pass. A few highlights worth calling out:

  • user.id presence check (fail-closed) — This is the right final guard. A provider returning valid: true with a user object that has no .id is still an unusable identity for any downstream context key (userId in generate/stream context, session storage key, rate-limit bucket). Throwing InvalidTokenError here prevents silent propagation of a "user without an identity."

  • authInitPromise poisoning fix — Clearing the promise on rejection (and on setAuthProvider() calls) is the correct retry model. Without this, a transient init failure would permanently poison the promise reference, making the provider permanently unusable for the lifetime of the NeuroLink instance.

  • ESM require() fix in setAuthContext/getAuthContext/clearAuthContext — Converting from sync require() to async import() is critical for ESM compatibility. Synchronous require() in an ESM module is a runtime error in Node.js native ESM mode, so this fix unblocks any consumers using "type": "module".

  • Async custom extractor in authProvider.ts — Handling Promise-returning custom extractors is important for any real-world use case where token extraction requires an async operation (e.g., decrypting a cookie, looking up a token from a store). The previous sync-only path would have silently discarded the promise value.

  • CognitoProvider.getUser() returning null — Correcting this to return null instead of throwing aligns with the MastraAuthProvider interface contract. Throwing from getUser() on a not-found/not-implemented case would be a contract violation that callers couldn't reliably distinguish from a transport error.

  • firebase.ts session storage delegation — Consistent with the pattern from Cycle 7 (jwt, oauth2, custom, betterAuth). Having all providers delegate to BaseAuthProvider's sessionStorage eliminates parallel implementations that would diverge over time.

  • Redis initPromise singleton in rateLimitByUser.ts — Mirrors the pattern in RedisSessionStorage. Prevents duplicate client connections under concurrent request load at startup.

Triggering an incremental review of the Cycle 8 changes now.


🧠 Learnings used
Learnt from: murdore
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2026-03-22T06:45:02.738Z
Learning: In juspay/neurolink, the canonical location for auth-related TypeScript types is `src/lib/types/authTypes.ts`. The previously existing `src/lib/auth/types/authTypes.ts` was a duplicate and has been deleted in PR `#892`. All auth type imports should reference `src/lib/types/authTypes.ts` (or its barrel re-export via `src/lib/types/index.ts`).

Learnt from: RajuSudhar
Repo: juspay/neurolink PR: 227
File: src/lib/utils/redis.ts:15-15
Timestamp: 2025-11-05T20:31:04.103Z
Learning: In the juspay/neurolink repository, type centralization rules (requiring types in src/lib/types/) do not apply to private, non-exported utility types used within a single file. Simple readability helpers like `type RedisClient = ReturnType<typeof createClient>` should remain in their implementation file when they are not exported and only used locally. Only exported types shared across modules, business/domain types, and public API types require centralization.

Learnt from: CR
Repo: juspay/neurolink PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T04:58:08.495Z
Learning: Applies to src/lib/{providers,types}/**/*.ts : No circular dependencies allowed. All providers use dynamic imports in registry, and type organization must prevent circular imports.

Learnt from: RajuSudhar
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2025-11-04T22:14:18.719Z
Learning: In the juspay/neurolink repository, do not flag existing type or interface definitions located outside src/lib/types/ - these are part of a phased migration plan and will be addressed in upcoming PRs. Only enforce type centralization rules on new code going forward.

Learnt from: RajuSudhar
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2025-11-04T22:14:18.719Z
Learning: In the juspay/neurolink repository, all new type definitions must be placed in src/lib/types/. New type definitions outside this directory should be flagged and blocked in code reviews.

Learnt from: murdore
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2026-01-02T19:49:21.526Z
Learning: Project standard: Place reusable/shared types under src/lib/types/*.ts; test-only helper types under test/types/*.ts; avoid declaring local types inside source implementation files.

Learnt from: CR
Repo: juspay/neurolink PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T04:58:08.495Z
Learning: Applies to src/lib/providers/**/*.ts : Providers must support consistent core interface across all 12+ providers (OpenAI, Anthropic, Google AI Studio, AWS Bedrock, Azure, Vertex, Mistral, LiteLLM, SageMaker, Hugging Face, Ollama, OpenAI-compatible).

Learnt from: CR
Repo: juspay/neurolink PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T04:58:08.495Z
Learning: Applies to src/lib/providers/**/*.ts : All 12+ providers must support consistent core interface. When implementing new providers, ensure compatibility with existing provider patterns and test provider consistency across all providers.

Learnt from: CR
Repo: juspay/neurolink PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T04:58:08.495Z
Learning: Applies to src/lib/types/**/*.ts : All type definitions must be organized by domain (providers, generation, streaming, MCP, conversation, tools, common) in src/lib/types/ to avoid circular dependencies.

Learnt from: RajuSudhar
Repo: juspay/neurolink PR: 173
File: src/lib/index.ts:16-16
Timestamp: 2025-09-17T17:55:15.261Z
Learning: In src/lib/types/providers.ts, ProviderConfig was renamed to AIModelProviderConfig to deduplicate type names, as there was an existing ProviderConfig type that better suited the "ProviderConfig" name. This was an intentional breaking change for better type organization.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

♻️ Duplicate comments (13)
src/lib/auth/authProvider.ts (1)

48-67: ⚠️ Potential issue | 🟠 Major

Consolidate this into the canonical BaseAuthProvider.

This second base class is still live: src/lib/auth/providers/supabase.ts, src/lib/auth/providers/custom.ts, and src/lib/auth/providers/jwt.ts extend it, while src/lib/auth/providers/CognitoProvider.ts extends src/lib/auth/providers/BaseAuthProvider.ts. The two bases already diverge on extraction, RBAC, session binding, and storage behavior, so provider behavior now depends on which import path a file chose. Based on learnings: All 12+ providers must support consistent core interface across all providers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/authProvider.ts` around lines 48 - 67, There are two divergent
base classes causing inconsistent provider behavior; consolidate into the
canonical BaseAuthProvider in BaseAuthProvider (class BaseAuthProvider in
src/lib/auth/authProvider.ts) by merging any logic from the alternate base
(src/lib/auth/providers/BaseAuthProvider.ts) into this class: ensure
tokenExtraction defaults, RBAC hooks, session binding, and sessionStorage
(InMemorySessionStorage) behavior are unified under the single BaseAuthProvider,
update MastraAuthProvider/AuthProviderConfig types as needed, and change all
providers (supabase.ts, custom.ts, jwt.ts, CognitoProvider.ts) to extend this
consolidated BaseAuthProvider and remove or redirect the duplicate base to
prevent split implementations.
src/lib/auth/authContext.ts (1)

66-68: ⚠️ Potential issue | 🟠 Major

Keep the process-wide fallback out of request handling.

These helpers fall back to globalAuthContext, but the provided src/lib/auth/middleware/AuthMiddleware.ts snippets only return or attach the authenticated context and never call runWithAuthContext(). Any server path that uses this singleton as the escape hatch therefore exposes one mutable auth slot to all overlapping requests.

Also applies to: 85-109, 373-375

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/authContext.ts` around lines 66 - 68, The code currently falls
back to a process-wide globalAuthContext in getAuthContext (and the related
helpers around requireAuthContext and any getters at the other noted spots),
which exposes a shared mutable auth slot; remove the fallback so getAuthContext
returns only authContextStorage.getStore() and update requireAuthContext and any
other accessor functions to rely exclusively on the request-scoped
authContextStorage (or throw/return undefined) and stop reading from
globalAuthContext.get(); keep globalAuthContext only as an explicit, opt-in API
(e.g., via a runWithAuthContext helper) and add/update callers to explicitly use
runWithAuthContext when process-wide behavior is actually intended.
src/lib/auth/providers/BaseAuthProvider.ts (1)

800-807: ⚠️ Potential issue | 🟠 Major

Match session reuse to the current token/session, not just user.id.

Reusing the first active session for the user can attach another device or token's session.id to this request. That breaks refresh, logout, and audit flows as soon as a user has more than one valid session.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/BaseAuthProvider.ts` around lines 800 - 807, The code
currently picks any valid session for the user (existingSessions.find(...))
which can incorrectly reuse another device/token's session; change the selection
to prefer an existing session that matches the incoming authentication result's
session identifier (e.g., result.sessionId or result.session?.id) and only fall
back to creating a new session if no matching valid session exists; update the
logic in BaseAuthProvider where existingSessions, validSession,
sessionStorage.getForUser, and createSession are used so you check session.id
=== result.sessionId (or the equivalent field on result) in addition to
s.isValid and expiry before reusing it.
src/lib/auth/providers/supabase.ts (1)

172-175: ⚠️ Potential issue | 🟠 Major

Merge the standard Supabase role claim with app_metadata.roles.

Supabase tokens commonly include role: "authenticated", so this ternary drops every application role from app_metadata.roles on the normal path. RBAC then sees only authenticated.

♻️ Suggested fix
-      roles: payload.role
-        ? [payload.role as string]
-        : (appMetadata?.roles as string[]) || [],
+      roles: Array.from(
+        new Set([
+          ...(typeof payload.role === "string" ? [payload.role] : []),
+          ...((appMetadata?.roles as string[] | undefined) ?? []),
+        ]),
+      ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/supabase.ts` around lines 172 - 175, The current logic
for building the roles array uses payload.role alone and discards
appMetadata.roles when payload.role exists; update the construction in the roles
field so it merges payload.role (if present) with any appMetadata.roles instead
of choosing one or the other: collect payload.role into an array (e.g.,
[payload.role]) and concat or merge it with (appMetadata?.roles as string[]) ||
[], avoiding duplicates so RBAC receives both the standard Supabase role and any
application roles defined in appMetadata.
src/lib/server/routes/agentRoutes.ts (2)

70-80: ⚠️ Potential issue | 🟠 Major

Caller-supplied IDs still flow into trusted context when unauthenticated.

The conditional logic improves the authenticated path, but when ctx.user is absent (unauthenticated request), request.userId and request.sessionId still populate context.userId and context.sessionId. If downstream code (memory, rate limiting, auditing) treats these fields as trusted identity, unauthenticated callers can impersonate arbitrary users.

Consider either:

  1. Only populating userId/sessionId from authenticated sources, leaving them undefined for anonymous requests
  2. Separating untrusted caller-provided IDs into distinct fields (e.g., rawRequestUserId)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/server/routes/agentRoutes.ts` around lines 70 - 80, The context
construction currently allows unauthenticated callers to inject request.userId
and request.sessionId into trusted context fields; update the object built in
agentRoutes.ts so that context.sessionId and context.userId are only set when
ctx.user is present (e.g., sessionId: ctx.user ? ctx.session?.id : undefined,
userId: ctx.user ? ctx.user.id : undefined) or, if you want to preserve caller
values, move them into separate untrusted fields like rawRequestUserId and
rawRequestSessionId instead of populating the trusted context properties; ensure
references to ctx.user, request.userId, and request.sessionId are adjusted
accordingly.

137-147: ⚠️ Potential issue | 🟠 Major

Same impersonation concern applies to the stream endpoint.

This block mirrors the execute handler's context construction. The same risk of caller-supplied IDs being used as trusted identity applies here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/server/routes/agentRoutes.ts` around lines 137 - 147, The stream
endpoint's context construction currently risks using caller-supplied IDs
(request.sessionId/request.userId) even when an authenticated ctx.user exists;
update the context building in the stream handler so that if ctx.user is present
you always use ctx.session?.id and ctx.user.id (and ctx.user.email /
ctx.user.roles) and never fall back to request.* values, otherwise fall back to
request.sessionId/request.userId when no ctx.user exists; adjust the assignments
for sessionId, userId, userEmail, and userRoles accordingly in the context
object (look for context, sessionId, userId, userEmail, userRoles, ctx.user,
ctx.session, request.sessionId, request.userId).
src/lib/neurolink.ts (1)

10992-11018: ⚠️ Potential issue | 🟠 Major

Prevent stale lazy auth init from overwriting a newer provider.

Clearing authInitPromise in setAuthProvider() does not cancel an older ensureAuthProvider() already in flight. If that older init resolves later, it still writes this.authProvider and can clobber a manual override. Gate the final assignment behind an init generation/token and ignore stale completions.

Also applies to: 11041-11055

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 10992 - 11018, setAuthProvider currently
clears authInitPromise but doesn't prevent an in-flight ensureAuthProvider
completion from overwriting a newer provider; introduce a generation/token
(e.g., this.authInitGeneration: number) incremented whenever setAuthProvider is
called or a new init starts, capture the current generation in the local scope
inside ensureAuthProvider and in any async init paths, and before assigning
this.authProvider (and before clearing this.authInitPromise) compare the
captured generation to this.authInitGeneration and bail out if they differ to
ignore stale completions; apply the same generation-check pattern to the other
init path referenced around lines 11041-11055 so any late-resolving async
create() or factory results do not clobber a later manual override.
src/lib/auth/middleware/AuthMiddleware.ts (2)

103-133: ⚠️ Potential issue | 🟠 Major

Honor fromHeader.scheme instead of always falling back to Bearer.

extractToken() only reads prefix, so fromHeader: { scheme: "Token" } still behaves like Bearer auth. Custom header schemes are effectively broken unless callers keep using the old field name.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/middleware/AuthMiddleware.ts` around lines 103 - 133, The header
extraction is only using headerConfig.prefix and ignores a provided
fromHeader.scheme, breaking custom schemes; update the logic in the extractToken
(AuthMiddleware) flow to prefer headerConfig.scheme (if defined) and fall back
to headerConfig.prefix and then "Bearer" only if neither is set. Concretely,
when computing the scheme used to validate the header, replace the current const
prefix = headerConfig.prefix ?? "Bearer" with something like const scheme =
headerConfig.scheme ?? headerConfig.prefix ?? "Bearer", use scheme for the
empty-scheme check (return raw value when scheme is falsy) and for building
schemeWithSpace (scheme + " "), and perform the case-insensitive comparison
against schemeWithSpace instead of prefixWithSpace so custom fromHeader.scheme
values are honored.

409-440: ⚠️ Potential issue | 🟠 Major

Exact permission checks still ignore wildcard grants.

Set.has() here will not match *, tools:*, or other hierarchical permissions, so createProtectedMiddleware() can deny users that the shared auth authorization logic would allow. Reuse the common permission matcher here instead of exact string comparison.

Also applies to: 499-503

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/middleware/AuthMiddleware.ts` around lines 409 - 440, The
effectivePermissions set is built from user.permissions and rolePermissions but
later permission checks (e.g., in createProtectedMiddleware) use exact Set.has()
so wildcard grants like '*' or 'tools:*' are ignored; update the authorization
checks to reuse the shared permission matcher (the project's common permission
matching function) instead of Set.has(), and where
expandRoles/rolePermissions/effectivePermissions are used, ensure you compare
requested permissions by iterating effectivePermissions and calling the common
matcher (e.g., permissionMatches(requested, granted)) to allow
wildcard/hierarchical matches; apply the same replacement to the other
occurrence referenced near createProtectedMiddleware so all checks use the
common matcher.
src/cli/commands/authProviders.ts (1)

128-133: ⚠️ Potential issue | 🟡 Minor

JWT config guidance is now inaccurate.

The CLI metadata says JWT requires secret, but buildProviderConfig() accepts publicKey alone. auth providers and the missing-config message will mislead valid JWT_PUBLIC_KEY-only setups.

Suggested fix
   jwt: {
     name: "JWT",
     description: "Generic JWT token validation with configurable secret/keys",
-    requiredConfig: ["secret"],
+    requiredConfig: ["secret or publicKey"],
     website: "https://jwt.io/",
   },

Also applies to: 229-243, 537-549

src/lib/auth/providers/workos.ts (1)

453-462: ⚠️ Potential issue | 🟠 Major

Re-throw lookup failures from getUserByEmail().

This catch still turns transport/retry errors into null, so a WorkOS outage looks the same as “user not found”. Only an empty result should return null; everything else should propagate.

Suggested fix
     } catch (error) {
       logger.error(
         "Failed to fetch WorkOS user by email:",
         error instanceof Error ? error.message : String(error),
       );
-      if (error instanceof ProviderAPIError) {
-        throw error;
-      }
-      return null;
+      throw error;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/workos.ts` around lines 453 - 462, The catch in
getUserByEmail currently logs errors but swallows non-ProviderAPIError
exceptions by returning null, which hides transport/retry failures; update the
catch in getUserByEmail to log and then re-throw the caught error (preserving
ProviderAPIError behavior) instead of returning null — only allow an explicit
empty API result path to return null, but do not convert exceptions into a null
"not found" result.
src/lib/auth/providers/oauth2.ts (1)

146-171: ⚠️ Potential issue | 🔴 Critical

Require exact iss and aud claims on the JWKS path.

This branch only checks iss when it exists and only compares its origin. A signature-valid token from another tenant/path on the same host—or one with no audience claim at all—can still pass here. This likely needs an explicit expected issuer in OAuth2Config instead of deriving only from authorizationUrl.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/oauth2.ts` around lines 146 - 171, The token
validation currently only optionally checks payload.iss and compares origins and
treats payload.aud as optional; update the logic in the try block that handles
jose.jwtVerify to require exact issuer and audience matches: add or use an
explicit expected issuer configuration on the OAuth2Config (e.g.,
expectedIssuer) and fail validation if payload.iss is missing or not exactly
equal to that expectedIssuer (fall back to a strict match against new
URL(this.authorizationUrl).origin only if expectedIssuer is not set), and fail
if payload.aud is missing or does not include this.clientId; update the error
messages to reflect missing vs. mismatched claims and reference the symbols
jwks, authorizationUrl, clientId, and the OAuth2Config expectedIssuer when
locating code to change.
src/lib/auth/AuthProviderRegistry.ts (1)

300-309: ⚠️ Potential issue | 🟡 Minor

Use typed error in the placeholder factory.

The factory function registered here throws a raw Error, bypassing the typed error surface. Per coding guidelines, use AuthRegistryError for consistency.

Proposed fix
     this.register(
       type,
       async () => {
-        throw new Error(
-          `Direct provider creation not supported. Use AuthProviderFactory.create() with config.`,
+        throw AuthRegistryError.create(
+          "REGISTRATION_FAILED",
+          "Direct provider creation is unsupported. Use createProvider(type, config).",
         );
       },
       metadata.aliases,
       { metadata },
     );

As per coding guidelines: "Use ErrorFactory for typed errors."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/AuthProviderRegistry.ts` around lines 300 - 309, Replace the raw
Error thrown in the placeholder factory registered via
AuthProviderRegistry.register with a typed AuthRegistryError created through the
ErrorFactory; locate the async factory passed to register (the one that
currently does throw new Error(...)) and change it to throw
ErrorFactory.create(AuthRegistryError, "Direct provider creation not supported.
Use AuthProviderFactory.create() with config.") or the project's equivalent
ErrorFactory usage so the registered factory throws the typed AuthRegistryError
instead of a plain Error.
🧹 Nitpick comments (1)
src/lib/auth/providers/clerk.ts (1)

56-57: Clerk uses provider-local Maps instead of shared session storage.

Unlike FirebaseAuthProvider which delegates to this.sessionStorage, ClerkProvider maintains its own in-memory sessions and userSessions Maps. This bypasses any shared session backend configured via config.session.customStorage, breaking consistency across providers.

Consider delegating session operations to this.sessionStorage (inherited from BaseAuthProvider) as Firebase does, or document this as an intentional provider-specific behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/clerk.ts` around lines 56 - 57, ClerkProvider
currently uses its own private maps (sessions and userSessions) instead of the
shared sessionStorage on BaseAuthProvider; replace usages of the private maps
with calls to the inherited this.sessionStorage API (the same pattern used by
FirebaseAuthProvider) so session create/read/update/delete and user->session
mappings use the configured customStorage backend, or if provider-specific
in-memory behavior is intentional, add a clear comment in the ClerkProvider
class documenting that sessions and userSessions are intentionally local and
will not use this.sessionStorage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/features/index.md`:
- Line 28: The table entry for "Authentication Providers" uses an absolute path;
update the link target for the "Authentication Providers" table cell to use a
relative path consistent with the other entries (e.g., change
/docs/features/authentication-providers to a relative link like
./authentication-providers or authentication-providers.md) so it points to the
existing docs/features/authentication-providers.md file and matches the
surrounding link format.

In `@src/lib/auth/middleware/AuthMiddleware.ts`:
- Around line 284-303: AuthMiddleware currently treats a token as authenticated
if validationResult.user exists even when user.id is empty; update the guard in
the validation flow (the block checking validationResult.user) to also require a
non-empty user.id (or equivalent unique identifier) and treat missing/empty id
as authentication failure: construct the same AuthError (statusCode 401, message
like "Token valid but no user identity resolved", code "AUTH-001"), call
config.onError with an Error augmented with code when present (as currently
done), and return { proceed: false, error } so requests with user objects
lacking an id are rejected; locate the check around validationResult.user in
AuthMiddleware to make this change.

In `@src/lib/auth/middleware/rateLimitByUser.ts`:
- Around line 194-206: In createClient(), register an 'error' event listener on
the newly created Redis client before calling client.connect() so Node Redis v5
won't emit unhandled errors; specifically, in the createClient() method add
client.on('error', (err) => { /* log via your logger or reject/initPromise
cleanup */ }) immediately after const client = createClient({ url: this.redisUrl
}) and before await client.connect(), and ensure you still clear initPromise and
throw the same error if connection fails (preserve existing initPromise cleanup
behavior).

In `@src/lib/auth/providers/clerk.ts`:
- Around line 176-187: The Clerk provider calls to the external API must be
bounded by a 5s timeout: update validateSessionToken(), getUser(),
getUserByEmail(), and healthCheck() to wrap their async fetch calls (the ones
using createProxyFetch()) with the shared withTimeout helper set to 5000 ms (or
5_000) so the promise rejects after 5s; locate the proxyFetch invocation in each
method (e.g., the POST to "https://api.clerk.com/v1/sessions/verify" in
validateSessionToken) and replace the direct await call with await
withTimeout(<existing proxyFetch call>, 5000) so all external Clerk requests
obey the timeout.

In `@src/lib/auth/providers/firebase.ts`:
- Around line 94-103: The non-null assertion this.jwks! used when calling
jose.jwtVerify(token, this.jwks!, ...) should be removed; instead ensure a
definite JWKS reference by capturing the result of initialization into a local
variable (e.g., call await this.initialize() when this.jwks is falsy, then set
const jwks = this.jwks) and pass that local jwks to jose.jwtVerify along with
token and the issuer/audience (use projectId as before) so static analysis no
longer needs the unsafe assertion and the code still guarantees a valid key set
for jwtVerify.

In `@src/lib/auth/providers/KeycloakProvider.ts`:
- Around line 193-214: The parser currently accepts tokens with no subject
because extractKeycloakUser() falls back to an empty id; update the
KeycloakProvider token validation to reject JWTs that lack a non-empty
claims.sub before returning valid: true (i.e., in the block that builds the
result object using extractKeycloakUser and validClaims), and return valid:
false (with no user) when claims.sub is missing or an empty string; apply the
same check to the other similar token-return path that also uses
extractKeycloakUser so tokens without a stable subject are fail-closed.

In `@src/lib/auth/providers/supabase.ts`:
- Around line 56-57: The Supabase provider is forking session state into private
maps (sessions and userSessions) which bypasses the shared sessionStorage and
breaks allowMultipleSessions, maxSessionsPerUser, revocation and any
custom/Redis-backed storage; remove these private maps and stop reading/writing
to sessions and userSessions, and instead use the shared sessionStorage API used
by the rest of the auth layer (the same session create/get/delete/update calls)
so all session lifecycle logic (allowMultipleSessions, maxSessionsPerUser,
revoke semantics) is preserved; update all methods that reference sessions or
userSessions (including the blocks you noted around 209–239 and 244–326) to
delegate to the central sessionStorage implementation and ensure revocation and
session limits are enforced via that shared interface.

In `@src/lib/auth/sessionManager.ts`:
- Around line 427-455: createSession currently always inserts a new session and
ignores SessionConfig limits; before creating the session, retrieve the user's
current sessions (e.g., via this.storage.getSessionsForUser or
this.storage.findByUser) and enforce this.config.allowMultipleSessions and
this.config.maxSessionsPerUser: if allowMultipleSessions is false, remove or
mark all existing sessions for the user invalid via the storage API before
calling this.storage.set; if maxSessionsPerUser is set and the new session would
exceed it, evict oldest sessions (remove/invalidate via storage) until the count
is within limit, then persist the new session and update logs accordingly (use
maskId(sessionId) and maskId(user.id) as currently done).

In `@src/lib/neurolink.ts`:
- Around line 3010-3017: withTimeout is being called with an Error instance
which violates its signature; change the third arg to a plain string (e.g.,
"Auth token validation timed out") and replace the hardcoded 5000 with
PROVIDER_TIMEOUTS.AUTH_MS; then move mapping to AuthenticationFailedError into
the surrounding catch that handles errors from withTimeout (wrap/convert the
caught TimeoutError or other errors into new AuthenticationFailedError using
this.authProvider.type) so the call becomes
withTimeout(this.authProvider.authenticateToken(options.auth.token),
PROVIDER_TIMEOUTS.AUTH_MS, "Auth token validation timed out") and authResult
handling/conversion happens in the catch.
- Around line 11067-11097: The public auth-context methods setAuthContext,
getAuthContext, and clearAuthContext should be synchronous: import the
auth/authContext.js module once at module scope (so you can reference
globalAuthContext and getAuthContext directly) and change these methods to
return void / AuthenticatedContext | undefined synchronously (remove async/await
and Promise return types), call globalAuthContext.set(context),
getAuthContext(), and globalAuthContext.clear() directly, and keep the same
logging behavior (logger.debug) so callers need not await these methods and you
avoid the race described.

---

Duplicate comments:
In `@src/lib/auth/authContext.ts`:
- Around line 66-68: The code currently falls back to a process-wide
globalAuthContext in getAuthContext (and the related helpers around
requireAuthContext and any getters at the other noted spots), which exposes a
shared mutable auth slot; remove the fallback so getAuthContext returns only
authContextStorage.getStore() and update requireAuthContext and any other
accessor functions to rely exclusively on the request-scoped authContextStorage
(or throw/return undefined) and stop reading from globalAuthContext.get(); keep
globalAuthContext only as an explicit, opt-in API (e.g., via a
runWithAuthContext helper) and add/update callers to explicitly use
runWithAuthContext when process-wide behavior is actually intended.

In `@src/lib/auth/authProvider.ts`:
- Around line 48-67: There are two divergent base classes causing inconsistent
provider behavior; consolidate into the canonical BaseAuthProvider in
BaseAuthProvider (class BaseAuthProvider in src/lib/auth/authProvider.ts) by
merging any logic from the alternate base
(src/lib/auth/providers/BaseAuthProvider.ts) into this class: ensure
tokenExtraction defaults, RBAC hooks, session binding, and sessionStorage
(InMemorySessionStorage) behavior are unified under the single BaseAuthProvider,
update MastraAuthProvider/AuthProviderConfig types as needed, and change all
providers (supabase.ts, custom.ts, jwt.ts, CognitoProvider.ts) to extend this
consolidated BaseAuthProvider and remove or redirect the duplicate base to
prevent split implementations.

In `@src/lib/auth/AuthProviderRegistry.ts`:
- Around line 300-309: Replace the raw Error thrown in the placeholder factory
registered via AuthProviderRegistry.register with a typed AuthRegistryError
created through the ErrorFactory; locate the async factory passed to register
(the one that currently does throw new Error(...)) and change it to throw
ErrorFactory.create(AuthRegistryError, "Direct provider creation not supported.
Use AuthProviderFactory.create() with config.") or the project's equivalent
ErrorFactory usage so the registered factory throws the typed AuthRegistryError
instead of a plain Error.

In `@src/lib/auth/middleware/AuthMiddleware.ts`:
- Around line 103-133: The header extraction is only using headerConfig.prefix
and ignores a provided fromHeader.scheme, breaking custom schemes; update the
logic in the extractToken (AuthMiddleware) flow to prefer headerConfig.scheme
(if defined) and fall back to headerConfig.prefix and then "Bearer" only if
neither is set. Concretely, when computing the scheme used to validate the
header, replace the current const prefix = headerConfig.prefix ?? "Bearer" with
something like const scheme = headerConfig.scheme ?? headerConfig.prefix ??
"Bearer", use scheme for the empty-scheme check (return raw value when scheme is
falsy) and for building schemeWithSpace (scheme + " "), and perform the
case-insensitive comparison against schemeWithSpace instead of prefixWithSpace
so custom fromHeader.scheme values are honored.
- Around line 409-440: The effectivePermissions set is built from
user.permissions and rolePermissions but later permission checks (e.g., in
createProtectedMiddleware) use exact Set.has() so wildcard grants like '*' or
'tools:*' are ignored; update the authorization checks to reuse the shared
permission matcher (the project's common permission matching function) instead
of Set.has(), and where expandRoles/rolePermissions/effectivePermissions are
used, ensure you compare requested permissions by iterating effectivePermissions
and calling the common matcher (e.g., permissionMatches(requested, granted)) to
allow wildcard/hierarchical matches; apply the same replacement to the other
occurrence referenced near createProtectedMiddleware so all checks use the
common matcher.

In `@src/lib/auth/providers/BaseAuthProvider.ts`:
- Around line 800-807: The code currently picks any valid session for the user
(existingSessions.find(...)) which can incorrectly reuse another device/token's
session; change the selection to prefer an existing session that matches the
incoming authentication result's session identifier (e.g., result.sessionId or
result.session?.id) and only fall back to creating a new session if no matching
valid session exists; update the logic in BaseAuthProvider where
existingSessions, validSession, sessionStorage.getForUser, and createSession are
used so you check session.id === result.sessionId (or the equivalent field on
result) in addition to s.isValid and expiry before reusing it.

In `@src/lib/auth/providers/oauth2.ts`:
- Around line 146-171: The token validation currently only optionally checks
payload.iss and compares origins and treats payload.aud as optional; update the
logic in the try block that handles jose.jwtVerify to require exact issuer and
audience matches: add or use an explicit expected issuer configuration on the
OAuth2Config (e.g., expectedIssuer) and fail validation if payload.iss is
missing or not exactly equal to that expectedIssuer (fall back to a strict match
against new URL(this.authorizationUrl).origin only if expectedIssuer is not
set), and fail if payload.aud is missing or does not include this.clientId;
update the error messages to reflect missing vs. mismatched claims and reference
the symbols jwks, authorizationUrl, clientId, and the OAuth2Config
expectedIssuer when locating code to change.

In `@src/lib/auth/providers/supabase.ts`:
- Around line 172-175: The current logic for building the roles array uses
payload.role alone and discards appMetadata.roles when payload.role exists;
update the construction in the roles field so it merges payload.role (if
present) with any appMetadata.roles instead of choosing one or the other:
collect payload.role into an array (e.g., [payload.role]) and concat or merge it
with (appMetadata?.roles as string[]) || [], avoiding duplicates so RBAC
receives both the standard Supabase role and any application roles defined in
appMetadata.

In `@src/lib/auth/providers/workos.ts`:
- Around line 453-462: The catch in getUserByEmail currently logs errors but
swallows non-ProviderAPIError exceptions by returning null, which hides
transport/retry failures; update the catch in getUserByEmail to log and then
re-throw the caught error (preserving ProviderAPIError behavior) instead of
returning null — only allow an explicit empty API result path to return null,
but do not convert exceptions into a null "not found" result.

In `@src/lib/neurolink.ts`:
- Around line 10992-11018: setAuthProvider currently clears authInitPromise but
doesn't prevent an in-flight ensureAuthProvider completion from overwriting a
newer provider; introduce a generation/token (e.g., this.authInitGeneration:
number) incremented whenever setAuthProvider is called or a new init starts,
capture the current generation in the local scope inside ensureAuthProvider and
in any async init paths, and before assigning this.authProvider (and before
clearing this.authInitPromise) compare the captured generation to
this.authInitGeneration and bail out if they differ to ignore stale completions;
apply the same generation-check pattern to the other init path referenced around
lines 11041-11055 so any late-resolving async create() or factory results do not
clobber a later manual override.

In `@src/lib/server/routes/agentRoutes.ts`:
- Around line 70-80: The context construction currently allows unauthenticated
callers to inject request.userId and request.sessionId into trusted context
fields; update the object built in agentRoutes.ts so that context.sessionId and
context.userId are only set when ctx.user is present (e.g., sessionId: ctx.user
? ctx.session?.id : undefined, userId: ctx.user ? ctx.user.id : undefined) or,
if you want to preserve caller values, move them into separate untrusted fields
like rawRequestUserId and rawRequestSessionId instead of populating the trusted
context properties; ensure references to ctx.user, request.userId, and
request.sessionId are adjusted accordingly.
- Around line 137-147: The stream endpoint's context construction currently
risks using caller-supplied IDs (request.sessionId/request.userId) even when an
authenticated ctx.user exists; update the context building in the stream handler
so that if ctx.user is present you always use ctx.session?.id and ctx.user.id
(and ctx.user.email / ctx.user.roles) and never fall back to request.* values,
otherwise fall back to request.sessionId/request.userId when no ctx.user exists;
adjust the assignments for sessionId, userId, userEmail, and userRoles
accordingly in the context object (look for context, sessionId, userId,
userEmail, userRoles, ctx.user, ctx.session, request.sessionId, request.userId).

---

Nitpick comments:
In `@src/lib/auth/providers/clerk.ts`:
- Around line 56-57: ClerkProvider currently uses its own private maps (sessions
and userSessions) instead of the shared sessionStorage on BaseAuthProvider;
replace usages of the private maps with calls to the inherited
this.sessionStorage API (the same pattern used by FirebaseAuthProvider) so
session create/read/update/delete and user->session mappings use the configured
customStorage backend, or if provider-specific in-memory behavior is
intentional, add a clear comment in the ClerkProvider class documenting that
sessions and userSessions are intentionally local and will not use
this.sessionStorage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f46ba752-cddc-4303-9edc-e33c93f4c7d4

📥 Commits

Reviewing files that changed from the base of the PR and between 0bfef96 and 9757568.

⛔ Files ignored due to path filters (2)
  • landing/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (49)
  • docs-site/sidebars.ts
  • docs/advanced/auth-architecture.md
  • docs/features/authentication-providers.md
  • docs/features/index.md
  • docs/superpowers/plans/2026-03-16-remove-redundant-tests.md
  • package.json
  • src/cli/commands/authProviders.ts
  • src/cli/factories/authCommandFactory.ts
  • src/cli/parser.ts
  • src/lib/auth/AuthProviderFactory.ts
  • src/lib/auth/AuthProviderRegistry.ts
  • src/lib/auth/RequestContext.ts
  • src/lib/auth/authContext.ts
  • src/lib/auth/authErrors.ts
  • src/lib/auth/authProvider.ts
  • src/lib/auth/index.ts
  • src/lib/auth/middleware/AuthMiddleware.ts
  • src/lib/auth/middleware/rateLimitByUser.ts
  • src/lib/auth/providers/BaseAuthProvider.ts
  • src/lib/auth/providers/CognitoProvider.ts
  • src/lib/auth/providers/KeycloakProvider.ts
  • src/lib/auth/providers/auth0.ts
  • src/lib/auth/providers/betterAuth.ts
  • src/lib/auth/providers/clerk.ts
  • src/lib/auth/providers/custom.ts
  • src/lib/auth/providers/firebase.ts
  • src/lib/auth/providers/jwt.ts
  • src/lib/auth/providers/oauth2.ts
  • src/lib/auth/providers/supabase.ts
  • src/lib/auth/providers/workos.ts
  • src/lib/auth/serverBridge.ts
  • src/lib/auth/sessionManager.ts
  • src/lib/core/infrastructure/baseRegistry.ts
  • src/lib/index.ts
  • src/lib/mcp/toolRegistry.ts
  • src/lib/neurolink.ts
  • src/lib/rag/ChunkerRegistry.ts
  • src/lib/rag/metadata/MetadataExtractorRegistry.ts
  • src/lib/rag/reranker/RerankerRegistry.ts
  • src/lib/server/routes/agentRoutes.ts
  • src/lib/types/authTypes.ts
  • src/lib/types/configTypes.ts
  • src/lib/types/generateTypes.ts
  • src/lib/types/index.ts
  • src/lib/types/streamTypes.ts
  • test/continuous-test-suite-auth.ts
  • test/fixtures/auth/provider-config.json
  • test/fixtures/auth/session-data.json
  • test/fixtures/auth/test-credentials.json
💤 Files with no reviewable changes (1)
  • docs/superpowers/plans/2026-03-16-remove-redundant-tests.md
✅ Files skipped from review due to trivial changes (9)
  • package.json
  • docs-site/sidebars.ts
  • src/lib/types/streamTypes.ts
  • test/fixtures/auth/test-credentials.json
  • test/fixtures/auth/provider-config.json
  • test/fixtures/auth/session-data.json
  • src/lib/auth/RequestContext.ts
  • src/lib/auth/providers/betterAuth.ts
  • src/lib/auth/authErrors.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • src/lib/rag/metadata/MetadataExtractorRegistry.ts
  • src/cli/parser.ts
  • src/lib/rag/ChunkerRegistry.ts
  • src/lib/types/configTypes.ts
  • src/lib/core/infrastructure/baseRegistry.ts
  • src/lib/auth/serverBridge.ts
  • src/lib/rag/reranker/RerankerRegistry.ts
  • src/cli/factories/authCommandFactory.ts
  • src/lib/types/index.ts
  • src/lib/mcp/toolRegistry.ts
  • src/lib/types/authTypes.ts
  • test/continuous-test-suite-auth.ts

Comment thread docs/features/index.md
| **[Context Compaction](context-compaction.md)** | 4-stage context compaction pipeline with automatic budget management, per-provider token estimation, and non-destructive message tagging. |
| **[Memory](memory.md)** | Per-user condensed memory that persists across conversations. LLM-powered condensation with S3, Redis, or SQLite storage backends. |
| **[Claude Subscription Support](claude-subscription.md)** | Multiple authentication methods for Claude (API key, OAuth) with support for Free, Pro, Max, and API tiers. |
| **[Authentication Providers](/docs/features/authentication-providers)** | Secure AI endpoints with 11 auth providers (Auth0, Clerk, Firebase, Supabase, Cognito, Keycloak, WorkOS, Better Auth, OAuth2, JWT, Custom) with RBAC, session management, and rate limiting. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the authentication-providers doc exists and what format other links use
fd -e md authentication-providers
# Check existing link formats in the features index
rg -n '\[.*\]\(' docs/features/index.md | head -20

Repository: juspay/neurolink

Length of output: 4661


Fix inconsistent link format for consistency with other entries in the table.

The Authentication Providers link uses an absolute-style path /docs/features/authentication-providers while all other table entries use relative paths. Since the file exists at docs/features/authentication-providers.md, use the relative path format for consistency:

Suggested change
-| **[Authentication Providers](/docs/features/authentication-providers)** | Secure AI endpoints with 11 auth providers (Auth0, Clerk, Firebase, Supabase, Cognito, Keycloak, WorkOS, Better Auth, OAuth2, JWT, Custom) with RBAC, session management, and rate limiting. |
+| **[Authentication Providers](authentication-providers.md)** | Secure AI endpoints with 11 auth providers (Auth0, Clerk, Firebase, Supabase, Cognito, Keycloak, WorkOS, Better Auth, OAuth2, JWT, Custom) with RBAC, session management, and rate limiting. |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| **[Authentication Providers](/docs/features/authentication-providers)** | Secure AI endpoints with 11 auth providers (Auth0, Clerk, Firebase, Supabase, Cognito, Keycloak, WorkOS, Better Auth, OAuth2, JWT, Custom) with RBAC, session management, and rate limiting. |
| **[Authentication Providers](authentication-providers.md)** | Secure AI endpoints with 11 auth providers (Auth0, Clerk, Firebase, Supabase, Cognito, Keycloak, WorkOS, Better Auth, OAuth2, JWT, Custom) with RBAC, session management, and rate limiting. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/features/index.md` at line 28, The table entry for "Authentication
Providers" uses an absolute path; update the link target for the "Authentication
Providers" table cell to use a relative path consistent with the other entries
(e.g., change /docs/features/authentication-providers to a relative link like
./authentication-providers or authentication-providers.md) so it points to the
existing docs/features/authentication-providers.md file and matches the
surrounding link format.

Comment on lines +284 to +303
// Fail closed: valid token without a user object is treated as failure
if (!validationResult.user) {
const error = {
statusCode: 401,
message: "Token valid but no user identity resolved",
code: "AUTH-001" as AuthErrorCode,
};

if (config.onError) {
await config.onError(
Object.assign(new Error(error.message), {
code: error.code,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any,
context,
);
}

return { proceed: false, error };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject “authenticated” users with no id.

This guard only checks that user exists. A provider can still return valid: true with user.id empty/undefined, and the middleware will treat that request as authenticated.

Suggested fix
-      if (!validationResult.user) {
+      if (!validationResult.user?.id) {
         const error = {
           statusCode: 401,
-          message: "Token valid but no user identity resolved",
+          message: "Token valid but no stable user identity resolved",
           code: "AUTH-001" as AuthErrorCode,
         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/middleware/AuthMiddleware.ts` around lines 284 - 303,
AuthMiddleware currently treats a token as authenticated if
validationResult.user exists even when user.id is empty; update the guard in the
validation flow (the block checking validationResult.user) to also require a
non-empty user.id (or equivalent unique identifier) and treat missing/empty id
as authentication failure: construct the same AuthError (statusCode 401, message
like "Token valid but no user identity resolved", code "AUTH-001"), call
config.onError with an Error augmented with code when present (as currently
done), and return { proceed: false, error } so requests with user objects
lacking an id are rejected; locate the check around validationResult.user in
AuthMiddleware to make this change.

Comment on lines +194 to +206
private async createClient(): Promise<RedisClient> {
try {
// Dynamic import to avoid loading Redis unless needed
const { createClient } = await import("redis");
const client = createClient({ url: this.redisUrl });
await client.connect();
this.client = client as unknown as RedisClient;
return this.client;
} catch {
this.initPromise = null;
throw new Error("Redis client not available for rate limiting");
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

node-redis v5 error listener requirement before connect

💡 Result:

Yes, in node-redis v5, you must attach an 'error' event listener to the client before calling connect. This prevents unhandled 'error' events from crashing the Node.js process, as per Node.js EventEmitter behavior. Official examples and docs show: import { createClient } from 'redis'; const client = createClient .on('error', err => console.error('Redis Client Error', err)) .connect; Without the listener, errors (e.g., connection failures, socket closures) emit as unhandled, terminating the process. This applies during and after connect, including reconnections. Docs emphasize: "You MUST listen to error events. If a client doesn't have at least one error listener registered and an error occurs, that error will be thrown and the Node.js process will exit." This requirement exists in v4 and continues unchanged in v5.

Citations:


🏁 Script executed:

# Check the actual code at lines 194-206 in rateLimitByUser.ts
fd "rateLimitByUser.ts" --type f -x cat -n {} | sed -n '190,210p'

Repository: juspay/neurolink

Length of output: 837


🏁 Script executed:

# Also check the imports at the top of the file to see if logger is available
fd "rateLimitByUser.ts" --type f -x head -50 {}

Repository: juspay/neurolink

Length of output: 1351


🏁 Script executed:

# Check if there are any error listeners registered elsewhere in the file
fd "rateLimitByUser.ts" --type f -x grep -n "\.on(" {}

Repository: juspay/neurolink

Length of output: 42


🏁 Script executed:

# Check getClient() and other methods to see if there's higher-level error handling
fd "rateLimitByUser.ts" --type f -x sed -n '170,195p' {}

Repository: juspay/neurolink

Length of output: 821


🏁 Script executed:

# Check if this client is used elsewhere and how errors might be handled
fd "rateLimitByUser.ts" --type f -x grep -n "this.client" {}

Repository: juspay/neurolink

Length of output: 297


Register Redis error listener before connect().

Node Redis v5 requires an error event listener before calling connect() to prevent unhandled errors from crashing the process. The initialization promise pattern is correct, but the error listener is missing.

Proposed fix
   private async createClient(): Promise<RedisClient> {
     try {
       // Dynamic import to avoid loading Redis unless needed
       const { createClient } = await import("redis");
       const client = createClient({ url: this.redisUrl });
+      
+      // Register error listener before connect (required by node-redis v5)
+      client.on("error", (err) => {
+        logger.warn("Redis rate limit client error:", err);
+      });
+      
       await client.connect();
       this.client = client as unknown as RedisClient;
       return this.client;
     } catch {
       this.initPromise = null;
       throw new Error("Redis client not available for rate limiting");
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/middleware/rateLimitByUser.ts` around lines 194 - 206, In
createClient(), register an 'error' event listener on the newly created Redis
client before calling client.connect() so Node Redis v5 won't emit unhandled
errors; specifically, in the createClient() method add client.on('error', (err)
=> { /* log via your logger or reject/initPromise cleanup */ }) immediately
after const client = createClient({ url: this.redisUrl }) and before await
client.connect(), and ensure you still clear initPromise and throw the same
error if connection fails (preserve existing initPromise cleanup behavior).

Comment on lines +499 to +512
// Check permissions (all required)
if (options.permissions && options.permissions.length > 0) {
const userPermissions = this.getEffectivePermissions(user);
const missingPermissions = options.permissions.filter(
(p) => !userPermissions.has(p),
);

if (missingPermissions.length > 0) {
result.authorized = false;
result.missingPermissions = missingPermissions;
result.reason = result.reason
? `${result.reason}; Missing permissions: ${missingPermissions.join(", ")}`
: `Missing required permissions: ${missingPermissions.join(", ")}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Honor wildcard permissions in authorize().

This exact Set.has check rejects users that only have "*" or "tools:*". authorizeUser() and authorizePermissions() delegate here, so providers using this base can deny requests that src/lib/auth/authContext.ts and the other auth base would authorize.

Comment on lines +176 to +187
const proxyFetch = createProxyFetch();
const response = await proxyFetch(
"https://api.clerk.com/v1/sessions/verify",
{
method: "POST",
headers: {
Authorization: `Bearer ${this.secretKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ token }),
},
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add timeout to external Clerk API calls.

The validateSessionToken() method calls the Clerk API without a timeout. Per the PR objectives, external provider calls should be bounded by a 5s timeout to prevent hung requests. The same applies to getUser(), getUserByEmail(), and healthCheck().

Proposed fix for validateSessionToken
       const proxyFetch = createProxyFetch();
       const response = await proxyFetch(
         "https://api.clerk.com/v1/sessions/verify",
         {
           method: "POST",
           headers: {
             Authorization: `Bearer ${this.secretKey}`,
             "Content-Type": "application/json",
           },
           body: JSON.stringify({ token }),
+          signal: AbortSignal.timeout(5000),
         },
       );

As per coding guidelines: "Use withTimeout for async operations."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const proxyFetch = createProxyFetch();
const response = await proxyFetch(
"https://api.clerk.com/v1/sessions/verify",
{
method: "POST",
headers: {
Authorization: `Bearer ${this.secretKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ token }),
},
);
const proxyFetch = createProxyFetch();
const response = await proxyFetch(
"https://api.clerk.com/v1/sessions/verify",
{
method: "POST",
headers: {
Authorization: `Bearer ${this.secretKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ token }),
signal: AbortSignal.timeout(5000),
},
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/clerk.ts` around lines 176 - 187, The Clerk provider
calls to the external API must be bounded by a 5s timeout: update
validateSessionToken(), getUser(), getUserByEmail(), and healthCheck() to wrap
their async fetch calls (the ones using createProxyFetch()) with the shared
withTimeout helper set to 5000 ms (or 5_000) so the promise rejects after 5s;
locate the proxyFetch invocation in each method (e.g., the POST to
"https://api.clerk.com/v1/sessions/verify" in validateSessionToken) and replace
the direct await call with await withTimeout(<existing proxyFetch call>, 5000)
so all external Clerk requests obey the timeout.

Comment on lines +193 to +214
// Extract user from claims
const user = this.extractKeycloakUser(claims);

// Convert claims to Record<string, JsonValue> by filtering out undefined
const validClaims: Record<
string,
import("../../types/common.js").JsonValue
> = {};
for (const [key, value] of Object.entries(claims)) {
if (value !== undefined) {
validClaims[key] = value;
}
}

return {
valid: true,
user,
claims: validClaims,
expiresAt: claims.exp ? new Date(claims.exp * 1000) : undefined,
issuer: claims.iss,
audience: claims.aud,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject JWTs that don't carry a subject.

extractKeycloakUser() falls back to id = "", so this path can return valid: true for a token with no stable user identity. That breaks fail-closed auth and can collapse sessions or per-user controls onto the empty id.

Suggested fix
-      // Extract user from claims
-      const user = this.extractKeycloakUser(claims);
+      if (!claims.sub) {
+        return {
+          valid: false,
+          error: "Token is missing required 'sub' claim",
+          errorCode: "AUTH-001",
+        };
+      }
+
+      // Extract user from claims
+      const user = this.extractKeycloakUser(claims);

Also applies to: 307-308

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/KeycloakProvider.ts` around lines 193 - 214, The
parser currently accepts tokens with no subject because extractKeycloakUser()
falls back to an empty id; update the KeycloakProvider token validation to
reject JWTs that lack a non-empty claims.sub before returning valid: true (i.e.,
in the block that builds the result object using extractKeycloakUser and
validClaims), and return valid: false (with no user) when claims.sub is missing
or an empty string; apply the same check to the other similar token-return path
that also uses extractKeycloakUser so tokens without a stable subject are
fail-closed.

Comment thread src/lib/auth/providers/supabase.ts Outdated
Comment on lines +56 to +57
private sessions: Map<string, AuthSession> = new Map();
private userSessions: Map<string, Set<string>> = new Map();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't fork session state into private maps here.

These maps sidestep the shared sessionStorage path, so allowMultipleSessions, maxSessionsPerUser, revocation behavior, and any custom or Redis-backed storage configured on the auth layer never apply to Supabase sessions. This provider will behave differently from the rest of the auth stack for the same session config.

Also applies to: 209-239, 244-326

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/providers/supabase.ts` around lines 56 - 57, The Supabase
provider is forking session state into private maps (sessions and userSessions)
which bypasses the shared sessionStorage and breaks allowMultipleSessions,
maxSessionsPerUser, revocation and any custom/Redis-backed storage; remove these
private maps and stop reading/writing to sessions and userSessions, and instead
use the shared sessionStorage API used by the rest of the auth layer (the same
session create/get/delete/update calls) so all session lifecycle logic
(allowMultipleSessions, maxSessionsPerUser, revoke semantics) is preserved;
update all methods that reference sessions or userSessions (including the blocks
you noted around 209–239 and 244–326) to delegate to the central sessionStorage
implementation and ensure revocation and session limits are enforced via that
shared interface.

Comment on lines +427 to +455
async createSession(
user: AuthUser,
metadata?: {
ipAddress?: string;
userAgent?: string;
deviceId?: string;
},
): Promise<AuthSession> {
const sessionId = crypto.randomUUID();
const now = new Date();
const duration = this.config.duration || 3600;

const session: AuthSession = {
id: sessionId,
user,
createdAt: now,
expiresAt: new Date(now.getTime() + duration * 1000),
isValid: true,
ipAddress: metadata?.ipAddress,
userAgent: metadata?.userAgent,
deviceId: metadata?.deviceId,
};

await this.storage.set(session);
logger.debug(
`Session created: ${maskId(sessionId)} for user: ${maskId(user.id)}`,
);

return session;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Enforce session limits in SessionManager.createSession().

This path ignores allowMultipleSessions and maxSessionsPerUser from SessionConfig and always inserts a new session. Consumers using SessionManager directly will therefore get unlimited concurrent sessions even when the same config would be enforced by provider-backed session creation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth/sessionManager.ts` around lines 427 - 455, createSession
currently always inserts a new session and ignores SessionConfig limits; before
creating the session, retrieve the user's current sessions (e.g., via
this.storage.getSessionsForUser or this.storage.findByUser) and enforce
this.config.allowMultipleSessions and this.config.maxSessionsPerUser: if
allowMultipleSessions is false, remove or mark all existing sessions for the
user invalid via the storage API before calling this.storage.set; if
maxSessionsPerUser is set and the new session would exceed it, evict oldest
sessions (remove/invalidate via storage) until the count is within limit, then
persist the new session and update logs accordingly (use maskId(sessionId) and
maskId(user.id) as currently done).

Comment thread src/lib/neurolink.ts
Comment on lines +3010 to +3017
authResult = await withTimeout(
this.authProvider.authenticateToken(options.auth.token),
5000,
new AuthenticationFailedError(
"Auth token validation timed out after 5000ms",
this.authProvider.type,
),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== withTimeout signature =="
sed -n '63,85p' src/lib/utils/async/withTimeout.ts

echo
echo "== generate()/stream() auth timeout call sites =="
sed -n '3010,3017p;5618,5625p' src/lib/neurolink.ts

Repository: juspay/neurolink

Length of output: 1365


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== PROVIDER_TIMEOUTS constant =="
rg "PROVIDER_TIMEOUTS|AUTH_MS" src/lib/neurolink.ts | head -20

echo
echo "== Surrounding code at line 3010 (context: 2995-3025) =="
sed -n '2995,3025p' src/lib/neurolink.ts

echo
echo "== Surrounding code at line 5618 (context: 5603-5633) =="
sed -n '5603,5633p' src/lib/neurolink.ts

Repository: juspay/neurolink

Length of output: 3045


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== PROVIDER_TIMEOUTS definition =="
rg "PROVIDER_TIMEOUTS\s*=" src/lib/ -A 5 --type ts

echo
echo "== AUTH_MS value =="
rg "AUTH_MS\s*:" src/lib/ --type ts

Repository: juspay/neurolink

Length of output: 704


Pass a string message to withTimeout(), not an error instance.

withTimeout() expects message?: string as the third argument. Passing new AuthenticationFailedError(...) violates the type contract and breaks strict TypeScript. The helper throws TimeoutError internally regardless—the passed error instance is ignored. Move the auth-error mapping to the surrounding catch block and pass a plain string message to withTimeout().

Additionally, replace the hardcoded 5000 timeout with PROVIDER_TIMEOUTS.AUTH_MS for consistency with other timeout values.

Suggested fix
-                authResult = await withTimeout(
-                  this.authProvider.authenticateToken(options.auth.token),
-                  5000,
-                  new AuthenticationFailedError(
-                    "Auth token validation timed out after 5000ms",
-                    this.authProvider.type,
-                  ),
-                );
+                authResult = await withTimeout(
+                  this.authProvider.authenticateToken(options.auth.token),
+                  PROVIDER_TIMEOUTS.AUTH_MS,
+                  `Auth token validation timed out after ${PROVIDER_TIMEOUTS.AUTH_MS}ms`,
+                );

Also applies to: 5618-5625

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 3010 - 3017, withTimeout is being called
with an Error instance which violates its signature; change the third arg to a
plain string (e.g., "Auth token validation timed out") and replace the hardcoded
5000 with PROVIDER_TIMEOUTS.AUTH_MS; then move mapping to
AuthenticationFailedError into the surrounding catch that handles errors from
withTimeout (wrap/convert the caught TimeoutError or other errors into new
AuthenticationFailedError using this.authProvider.type) so the call becomes
withTimeout(this.authProvider.authenticateToken(options.auth.token),
PROVIDER_TIMEOUTS.AUTH_MS, "Auth token validation timed out") and authResult
handling/conversion happens in the catch.

Comment thread src/lib/neurolink.ts
Comment on lines +11067 to +11097
async setAuthContext(context: AuthenticatedContext): Promise<void> {
const { globalAuthContext } = await import("./auth/authContext.js");
globalAuthContext.set(context);
logger.debug("Auth context set", {
userId: context.user.id,
provider: context.provider,
sessionId: context.session?.id,
});
}

/**
* Get the current authentication context.
*
* Checks AsyncLocalStorage first, then falls back to the global holder.
*/
async getAuthContext(): Promise<AuthenticatedContext | undefined> {
const { getAuthContext: getCtx } = await import("./auth/authContext.js");
return getCtx();
}

/**
* Clear the current authentication context
*/
async clearAuthContext(): Promise<void> {
const { globalAuthContext } = await import("./auth/authContext.js");
const userId = globalAuthContext.get()?.user.id;
globalAuthContext.clear();
if (userId) {
logger.debug(`Auth context cleared for user: ${userId}`);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if these methods existed before and their previous signatures
cd /tmp/repo && git log --oneline -n 20 -- src/lib/neurolink.ts | head -10

Repository: juspay/neurolink

Length of output: 119


🏁 Script executed:

# Check the current state of authContext.js to understand dependencies
cd /tmp/repo && cat -n src/lib/auth/authContext.js | head -50

Repository: juspay/neurolink

Length of output: 119


🏁 Script executed:

# Search for usage of these auth context methods in the codebase
cd /tmp/repo && rg "setAuthContext|getAuthContext|clearAuthContext" --type ts --type js -A 2 -B 2 | head -100

Repository: juspay/neurolink

Length of output: 119


🏁 Script executed:

# Check if there's top-level await in neurolink.ts and how imports are currently done
cd /tmp/repo && head -100 src/lib/neurolink.ts | grep -E "(^import|^export|await|top-level)" -A 1

Repository: juspay/neurolink

Length of output: 119


🏁 Script executed:

# Check if authContext is already imported at module scope anywhere
cd /tmp/repo && rg "authContext" src/lib/neurolink.ts | head -20

Repository: juspay/neurolink

Length of output: 119


🏁 Script executed:

# Check the current state of authContext.js to understand dependencies
cat -n src/lib/auth/authContext.js | head -60

Repository: juspay/neurolink

Length of output: 119


🏁 Script executed:

# Search for usage of these auth context methods in the codebase
rg "setAuthContext|getAuthContext|clearAuthContext" -A 2 -B 2 | head -150

Repository: juspay/neurolink

Length of output: 5935


🏁 Script executed:

# Check if there's top-level await in neurolink.ts and how imports are currently done
head -150 src/lib/neurolink.ts | grep -E "(import|export|await)" -A 1

Repository: juspay/neurolink

Length of output: 3349


🏁 Script executed:

# Check how authContext is currently referenced in neurolink.ts
rg "authContext" src/lib/neurolink.ts -B 2 -A 2 | head -100

Repository: juspay/neurolink

Length of output: 1853


🏁 Script executed:

# Get file structure to understand where auth context is located
git ls-files | grep -i auth | head -20

Repository: juspay/neurolink

Length of output: 786


🏁 Script executed:

# Check the authContext.ts file structure
cat -n src/lib/auth/authContext.ts | head -100

Repository: juspay/neurolink

Length of output: 3259


🏁 Script executed:

# Check if authContext.ts has any circular dependencies or async initialization
rg "import|export|async" src/lib/auth/authContext.ts | head -50

Repository: juspay/neurolink

Length of output: 1303


🏁 Script executed:

# Look for how getAuthContext is currently imported/used in other parts of the SDK
rg "from.*authContext" src/lib --type ts | head -20

Repository: juspay/neurolink

Length of output: 193


🏁 Script executed:

# Check existing tests to see how setAuthContext/getAuthContext are used
rg "setAuthContext\|getAuthContext\|clearAuthContext" test --type ts -B 2 -A 2 | head -100

Repository: juspay/neurolink

Length of output: 42


🏁 Script executed:

# Check if neurolink.ts already imports anything from auth/authContext.ts at module scope
grep -n "auth/authContext" src/lib/neurolink.ts

Repository: juspay/neurolink

Length of output: 301


🏁 Script executed:

# Check documentation to see if setAuthContext/getAuthContext are documented as sync
grep -A 5 "setAuthContext\|getAuthContext" docs/features/authentication-providers.md | head -30

Repository: juspay/neurolink

Length of output: 536


🏁 Script executed:

# Look for any migration guide or changelog that might indicate API change
fd -e md . docs | xargs grep -l "breaking\|migration\|changelog" 2>/dev/null | head -5

Repository: juspay/neurolink

Length of output: 228


🏁 Script executed:

# Check if there's a CHANGELOG or migration guide
fd -e md . . | xargs grep -l "setAuthContext" 2>/dev/null

Repository: juspay/neurolink

Length of output: 140


🏁 Script executed:

# Check the auth-architecture docs to understand intended design
cat -n docs/advanced/auth-architecture.md | head -100

Repository: juspay/neurolink

Length of output: 5125


🏁 Script executed:

# Look at the actual method signatures and comments in neurolink.ts for these methods
sed -n '11060,11100p' src/lib/neurolink.ts

Repository: juspay/neurolink

Length of output: 1483


🏁 Script executed:

# Check if there are any exports or API declarations that show the intended signature
rg "setAuthContext|getAuthContext|clearAuthContext" src/lib/index.ts -B 2 -A 2

Repository: juspay/neurolink

Length of output: 158


🏁 Script executed:

# Check if there's a git history showing these methods were previously sync
git log -p --follow -S "setAuthContext" -- src/lib/neurolink.ts | head -80

Repository: juspay/neurolink

Length of output: 42


🏁 Script executed:

# Verify globalAuthContext and related exports are available at module level
sed -n '100,150p' src/lib/auth/authContext.ts

Repository: juspay/neurolink

Length of output: 1230


🏁 Script executed:

# Check what globalAuthContext class looks like and verify it's synchronous
sed -n '150,200p' src/lib/auth/authContext.ts

Repository: juspay/neurolink

Length of output: 1175


🏁 Script executed:

# Double-check if there are any async operations in authContext initialization
tail -100 src/lib/auth/authContext.ts

Repository: juspay/neurolink

Length of output: 2013


🏁 Script executed:

# Verify the documentation shows sync behavior expected
cat docs/features/authentication-providers.md | grep -A 20 "setAuthContext"

Repository: juspay/neurolink

Length of output: 1128


Keep the auth-context API synchronous.

Making these public methods return Promises is a breaking SDK change, and setAuthContext() can now race with an immediate generate()/stream() call if callers do not await it. The underlying authContext module is fully synchronous (containing only sync functions like globalAuthContext.set(), getAuthContext()) and is already imported at module scope elsewhere (e.g., toolRegistry.ts). Load authContext.js once at module scope and keep the public methods sync to maintain backward compatibility and avoid race conditions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 11067 - 11097, The public auth-context
methods setAuthContext, getAuthContext, and clearAuthContext should be
synchronous: import the auth/authContext.js module once at module scope (so you
can reference globalAuthContext and getAuthContext directly) and change these
methods to return void / AuthenticatedContext | undefined synchronously (remove
async/await and Promise return types), call globalAuthContext.set(context),
getAuthContext(), and globalAuthContext.clear() directly, and keep the same
logging behavior (logger.debug) so callers need not await these methods and you
avoid the race described.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

…ddleware, RBAC, and session management

Implements a pluggable authentication layer following the Factory + Registry pattern:
- 12 auth providers: Auth0, Clerk, Firebase, Supabase, Cognito, Keycloak, BetterAuth, WorkOS, JWT, OAuth2/OIDC, Custom
- AuthMiddleware with token extraction (header/cookie/query), RBAC enforcement, public route bypass
- Rate limiting middleware (token bucket, per-user/per-role limits, memory + Redis backends)
- SessionManager with memory and Redis storage, auto-refresh
- AsyncLocalStorage-based auth context propagation
- SDK integration: NeuroLink constructor auth config, generate/stream requestContext and auth.token support
- CLI integration: auth providers/validate/health/login/logout/status/refresh commands
- 22 test cases across 4 sections (server auth, RBAC, per-call pre-validated, per-call token validation)
@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@murdore
murdore merged commit 97dabd9 into release Mar 25, 2026
18 checks passed
@murdore
murdore deleted the feat/authentication-providers branch March 25, 2026 23:24
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 9.32.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

This branch was successfully deployed

1 active deployment
Preview — 0013d914 Deployed Mar 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants