Skip to content

fix: gh-2517 change for fixing next edge runtime build error for crypto module#2564

Merged
Piyush-85 merged 9 commits intomainfrom
fix/gh-2517
Apr 15, 2026
Merged

fix: gh-2517 change for fixing next edge runtime build error for crypto module#2564
Piyush-85 merged 9 commits intomainfrom
fix/gh-2517

Conversation

@Piyush-85
Copy link
Copy Markdown
Contributor

Closes #2517

🎯 Problem Statement

Original Issue

Edge Runtime builds were failing with:

Module not found: Can't resolve 'crypto'

Root Cause

  • dpopUtils.ts had static import { crypto } at the top
  • Static imports cause bundlers (Webpack, Turbopack) to include ALL transitive dependencies
  • Even when useDPoP=false, the crypto module was being bundled
  • Edge Runtime doesn't support Node.js crypto module → build failure

✅ Solution Overview

Strategy

  1. File Splitting: Separate crypto-free utilities from crypto-dependent code
  2. Dynamic Imports: Load crypto-dependent code only when needed at runtime
  3. Lazy Validation: Defer DPoP configuration validation until first use
  4. Performance Optimization: Use flags to ensure validation only happens once

📊 Before vs After Comparison

BEFORE: Eager Validation (Static Imports)

Runtime Flow

new Auth0Client() called
  ↓
validateDpopConfiguration() runs IMMEDIATELY (constructor)
  ↓
Loads keys from env vars using crypto APIs
  ↓
crypto module MUST be bundled

Bundle Impact: crypto included even when useDPoP=false

AFTER: Lazy Validation (Dynamic Imports)

Runtime Flow

new Auth0Client() called
  ↓
Constructor completes (NO validation, NO crypto)
  ↓
... (application runs) ...
  ↓
User triggers login/callback/token operation
  ↓
await this.ensureDpopValidated() called
  ↓
First call: dynamic import + validation (crypto loaded)
Subsequent calls: immediate return (dpopValidated=true)

Bundle Impact: crypto ONLY included when useDPoP=true AND env vars present ✓

🔒 Error Timing Analysis

Scenario: Missing DPoP Environment Variables

// User code - IDENTICAL in both cases
const auth0 = new Auth0Client({ useDPoP: true });

Scenario: Invalid DPoP Keys (Invalid PEM format)

Before:

process.env.AUTH0_DPOP_PRIVATE_KEY = "invalid-key";
const auth0 = new Auth0Client({ useDPoP: true });
// ❌ Error thrown here: "Invalid PEM format"

After:

process.env.AUTH0_DPOP_PRIVATE_KEY = "invalid-key";
const auth0 = new Auth0Client({ useDPoP: true });
// ✅ Constructor succeeds
await auth0.handleLogin(request);
// ❌ Error thrown here: "Invalid PEM format"

Error timing shifted from construction to first operation

⚠️ Trade-offs

  1. Slightly Delayed Validation

    • Validation errors appear at first operation, not construction
    • Mitigation: Simple env var check in constructor warns users
  2. Additional File

    • Created dpopRetry.ts to separate concerns
    • Benefit: Clear separation of crypto/non-crypto code
  3. Dynamic Import Complexity

    • Adds async operation to validation
    • Benefit: Only happens once, minimal overhead

✅ Backward Compatibility

No breaking changes - User code works unchanged:

// Same before and after - no migration needed
import { Auth0Client } from '@auth0/nextjs-auth0/server';
export const auth0 = new Auth0Client();
await auth0.handleLogin(request);
  • Constructor remains synchronous
  • All method signatures unchanged
  • Same error types and messages
  • TypeScript types fully compatible

📋 Testing Checklist

  • All 888 unit tests pass
  • Lint clean (TypeScript + ESLint)
  • DPoP functionality works when enabled
  • crypto NOT bundled when useDPoP=false
  • Edge Runtime builds succeed
  • Validation only happens once per instance

📌 Summary

✅ Backward Compatible - No breaking changes, no migration needed
🚀 Performance Impact - Negligible (~1-5ms one-time validation on first DPoP operation)
📦 Bundle Size - Crypto module excluded when useDPoP=false (~50KB savings)

Key Files Changed:

  • src/utils/dpopRetry.ts (new) - Crypto-free utilities
  • src/utils/dpopUtils.ts - Now crypto-only, dynamically imported
  • src/server/client.ts - Removed static validation
  • src/server/auth-client.ts - Added lazy validation with ensureDpopValidated()

@Piyush-85 Piyush-85 requested a review from a team as a code owner March 18, 2026 20:24
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 18, 2026

Codecov Report

❌ Patch coverage is 99.19355% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.06%. Comparing base (8eb6cd2) to head (9e6f62d).

Files with missing lines Patch % Lines
src/utils/dpopRetry.ts 97.77% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2564      +/-   ##
==========================================
+ Coverage   89.97%   90.06%   +0.09%     
==========================================
  Files          62       63       +1     
  Lines        7543     7603      +60     
  Branches     1604     1608       +4     
==========================================
+ Hits         6787     6848      +61     
+ Misses        744      743       -1     
  Partials       12       12              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@tusharpandey13
Copy link
Copy Markdown
Contributor

tusharpandey13 commented Mar 28, 2026

Validation errors appear at first operation, not construction

Current logic will break some customer builds if DPoP config is wrong (we have had cases where custoemr builds failed if validation failed due to the way their build system handles errors). This change will allow builds to pass and isntead runtime errors to be thrown.
Not against this, just wanted to know if you think this would be okay to do.

Comment thread src/server/auth-client.test.ts Outdated
Comment thread src/server/client.ts Outdated
Comment thread src/server/auth-client.ts
Comment thread src/utils/dpopUtils.ts
Comment on lines +52 to +54
// String concatenation at runtime makes the import path impossible to statically analyze
// This is bundler-agnostic and works with webpack, Turbopack, Vite, esbuild, Rollup, etc.
const cryptoModule = "cry" + "pto";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

const cryptoModule = "cry" + "pto";

Why statically load this module when we are already dynamically importing dpopUtils.ts?
I think a simple import { createPrivateKey } from "node:crypto" should achieve the same, without the edge cases that this can bring.


This is bundler-agnostic and works with webpack, Turbopack, Vite, esbuild, Rollup, etc.

Are we sure that future changes to bundlers related to static analysis will not break this? Bundler choice and version is not in control of the SDK.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Have we taken any action on this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dynamic import alone did not solve this issue :

Bundlers perform static analysis on all reachable files during build time, independent of dynamic imports.

// auth-client.ts
await import("../utils/dpopUtils.js");  // Dynamic import

// dpopUtils.ts
import { createPrivateKey } from "crypto";  // Static import

Build process:

  • Bundler scans dpopUtils.ts during static analysis
  • Detects crypto import, attempts to bundle crypto module
  • Edge Runtime build fails (crypto not available)
  • The await import() only controls when the pre-bundled module loads, not whether its dependencies get bundled.

Why node:crypto Doesn't Solve This

await import("node:crypto"); // Still a static string

Bundlers detect the string literal "node:crypto" during static analysis and attempt module resolution. The node: prefix doesn't prevent this.

const cryptoModule = "cry" + "pto";
await import(cryptoModule);  // Path unknown to bundler

Static analysis cannot predict runtime string operations. Bundler sees import(variable) with unknown value and skips bundling.

Bundler Compatibility
Works across all JavaScript bundlers
No bundler can trace computed string values without runtime execution. This is not a workaround and is widely accepted.

Verified:
Internal testing confirmed Edge Runtime builds failed with static crypto imports and succeeded with runtime-constructed paths.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting to run this by @nandan-bhat once, to get consensus.

Copy link
Copy Markdown
Contributor

@tusharpandey13 tusharpandey13 left a comment

Choose a reason for hiding this comment

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

Please take a look at these comments, thanks.

@Piyush-85
Copy link
Copy Markdown
Contributor Author

Validation errors appear at first operation, not construction

Current logic will break some customer builds if DPoP config is wrong (we have had cases where custoemr builds failed if validation failed due to the way their build system handles errors). This change will allow builds to pass and isntead runtime errors to be thrown. Not against this, just wanted to know if you think this would be okay to do.

Yes, it is. But this is intentional and better because:

Error still gets caught - just at the right time (when DPoP is used)
Error is equally clear - same validation logic, same error message
Prevents false failures - builds don't fail for unused features
Matches industry practice - all major SDKs work this way
Better for real workflows - supports feature flags, multi-env deploys, gradual rollouts

The trade-off is:

Loss: Build-time config validation
Gain: Runtime flexibility, better DX, no false build failures

…y() params & auth-client dpop test improvements
@Piyush-85
Copy link
Copy Markdown
Contributor Author

Piyush-85 commented Apr 1, 2026

Hi @tusharpandey13,
I've addressed all the review comments. Please have a look when you get a chance.

Additional Fix: Critical DPoP Bug

Problem

  1. The implementation was calling toCryptoKey() with incorrect parameters:

    • Wrong algorithm format: Passed a JWT algorithm string instead of the Web Crypto algorithm object required by the API
    • Incorrect extractable flag: Set to false, which prevents JWK export needed for DPoP proof generation
  2. toCryptoKey() has inconsistent availability across different Node.js builds and environments.

The Solution
Switched to the standard Web Crypto API subtle.importKey() method instead of toCryptoKey():

Impact:

  • Environment variable loading has never worked for DPoP keypairs loaded from AUTH0_DPOP_PRIVATE_KEY and AUTH0_DPOP_PUBLIC_KEY which is fixed now.
  • Uses standard Web Crypto API which is consistent (guaranteed in Node 16+)

@Piyush-85 Piyush-85 force-pushed the fix/gh-2517 branch 3 times, most recently from d498c40 to 3753fbb Compare April 2, 2026 09:42
Comment thread src/server/auth-client.test.ts Outdated
Copy link
Copy Markdown
Contributor

@tusharpandey13 tusharpandey13 left a comment

Choose a reason for hiding this comment

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

I have left additional comments on previously resolved comments.
Please take a look, thanks.

@Piyush-85 Piyush-85 force-pushed the fix/gh-2517 branch 2 times, most recently from a003f1b to 1460b7b Compare April 2, 2026 12:48
Comment thread src/server/auth-client.test.ts
Comment thread src/server/auth-client.test.ts Outdated
Comment thread src/server/auth-client.test.ts Outdated
Comment thread src/server/auth-client.test.ts Outdated
Copy link
Copy Markdown
Contributor

@tusharpandey13 tusharpandey13 left a comment

Choose a reason for hiding this comment

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

Please take a look

tusharpandey13
tusharpandey13 previously approved these changes Apr 9, 2026
@Piyush-85 Piyush-85 merged commit fa90a53 into main Apr 15, 2026
9 checks passed
@Piyush-85 Piyush-85 deleted the fix/gh-2517 branch April 15, 2026 10:44
This was referenced Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Webpack built Next Edge runtime not compatible with this library

3 participants