js icon indicating copy to clipboard operation
js copied to clipboard

Yarn Build Edge Runtime Error: Dynamic Code Evaluation Not Allowed with thirdweb SDK

Open broomva opened this issue 1 year ago • 3 comments

Description

When deploying a Next.js application that utilizes the @thirdweb-dev/sdk and related packages, the deployment fails in environments using the Edge Runtime (such as Vercel) due to security restrictions against dynamic code evaluation methods like eval, new Function, and WebAssembly.compile. The error message indicates that these methods are used within the SDK, which is not permitted in the Edge Runtime environment.

Error Message:

Failed to compile.

./node_modules/@thirdweb-dev/sdk/dist/contract-publisher-1ff1fe07.browser.esm.js
Dynamic Code Evaluation (e.g., 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime
Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation

Import trace for requested module:
./node_modules/@thirdweb-dev/sdk/dist/contract-publisher-1ff1fe07.browser.esm.js
./node_modules/@thirdweb-dev/sdk/dist/thirdweb-dev-sdk.browser.esm.js
./node_modules/@thirdweb-dev/wallets/evm/wallets/ethers/dist/thirdweb-dev-wallets-evm-wallets-ethers.browser.esm.js
./node_modules/@thirdweb-dev/auth/next-auth/dist/thirdweb-dev-auth-next-auth.browser.esm.js
./auth.config.ts

> Build failed because of webpack errors
error Command failed with exit code 1.

Expected Behavior

The expectation is for the @thirdweb-dev/sdk to be compatible with Next.js applications deployed in environments using the Edge Runtime without encountering security policy violations related to dynamic code evaluation.

Steps to Reproduce

  1. Create a Next.js application that imports and utilizes @thirdweb-dev/sdk.
  2. Deploy the application to an environment that utilizes the Next.js Edge Runtime (e.g., Vercel).
  3. Observe the build failure due to the Edge Runtime's security restrictions.

Possible Solutions or Workarounds

I am seeking guidance on how to address or work around this issue, whether it involves updates to the SDK, configuration changes in the Next.js application, or alternative deployment strategies that are compatible with the Edge Runtime's security policies.

Environment

Next.js version: 14.1.3 @thirdweb-dev/sdk version: 4.0.54 Deployment environment: Vercel. Altought the build error does occur in local too

broomva avatar Apr 02 '24 06:04 broomva

@broomva thanks for the report, we'll take a look.

In general the path I can recommend here would be to upgrade to sdk v5 which is the new version of the SDK that is tested against edge environments.

That said this will definitely be a bigger change since we overhauled the SDK in a major way with v5. We believe it is a lot better, but I understand if this may not be a solution to your immediate problem.

In the case that upgrading to v5 is not an option would you be able to provide a minimal reproduction repo of the issue you're encountering? This would go a long way to us being able to hunt it down.

Thanks!

jnsdls avatar Apr 02 '24 06:04 jnsdls

Thanks for your rapid response @jnsdls, I think I can use the v5 if it includes the capabilities to leverage next-auth. The repo I'm working on is not public and its a rather large code base, but I share below the scripts that use thirdweb:

app/thirdweb.ts

'use client';

export { ThirdwebProvider } from "@thirdweb-dev/react";

app/layout.ts

import { ThirdwebProvider } from "@/app/thirdweb";

...

export default function RootLayout({ children }: RootLayoutProps) {

  return (
    <html lang="en" suppressHydrationWarning>
      <head />
      <body
        className={cn(
          "min-h-screen bg-background font-sans antialiased",
          fontSans.variable,
          fontUrban.variable,
          fontHeading.variable
        )}
      >
          <ThirdwebProvider
            clientId={process.env.THIRDWEB_SECRET || ""}
            authConfig={{
              domain: process.env.NEXT_PUBLIC_THIRDWEB_AUTH_DOMAIN || "",
            }}>
          {children}
          </ThirdwebProvider>
          <Analytics />
          <Toaster />
          <ModalProvider />
      </body>
    </html>
  )
}

auth.config.ts

import GitHubProvider from "next-auth/providers/github";
import Google from "next-auth/providers/google";
import type { NextAuthConfig } from "next-auth";
import { ThirdwebAuthProvider } from "@thirdweb-dev/auth/next-auth";

export default {
  providers: [
    Google({
      clientId: env.GOOGLE_CLIENT_ID,
      clientSecret: env.GOOGLE_CLIENT_SECRET,
      allowDangerousEmailAccountLinking: true 
    }),
    GitHubProvider({
      clientId: process.env.GITHUB_CLIENT_ID,
      clientSecret: process.env.GITHUB_CLIENT_SECRET,
      allowDangerousEmailAccountLinking: true 
    }),
    ThirdwebAuthProvider({
      domain: process.env.NEXT_PUBLIC_THIRDWEB_AUTH_DOMAIN || "",
    }),
  ],
} satisfies NextAuthConfig

components/forms/user-auth-form.tsx

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { signIn } from "next-auth/react"
import { useSearchParams } from "next/navigation"
import * as React from "react"
import { useForm } from "react-hook-form"
import * as z from "zod"

import { buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { userAuthSchema } from "@/lib/validations/auth"
import { Icons } from "@/components/shared/icons"
import { toast } from "@/components/ui/use-toast"
import {
  ConnectWallet,
  useAddress,
  useAuth
} from "@thirdweb-dev/react"

interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {
  type?: string
}

type FormData = z.infer<typeof userAuthSchema>

export function UserAuthForm({ className, type, ...props }: UserAuthFormProps) {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>({
    resolver: zodResolver(userAuthSchema),
  })
  const [isLoading, setIsLoading] = React.useState<boolean>(false)
  const [isGoogleLoading, setIsGoogleLoading] = React.useState<boolean>(false)
  const [isGitHubLoading, setIsGitHubLoading] = React.useState<boolean>(false)
  const [isWalletLoading, setIsWalletLoading] = React.useState<boolean>(false)
  const searchParams = useSearchParams()


  const auth = useAuth();
  const address = useAddress();

  const loginWithWallet = async () => {
    // Get the sign-in with ethereum login payload
    const payload = await auth?.login();
    // Use the payload to sign-in via our wallet based credentials provider
    await signIn("credentials", {
      payload: JSON.stringify(payload),
      redirect: true,
      callbackUrl: searchParams?.get("from") || "/dashboard",
    });
  };

  return (
    <div className={cn("grid gap-6", className)} {...props}>
      <button
        type="button"
        className={cn(buttonVariants({ variant: "outline" }))}
        onClick={() => {
          setIsGoogleLoading(true)
          signIn("google")
        }}
        disabled={isLoading || isGoogleLoading}
      >
        {isGoogleLoading ? (
          <Icons.spinner className="mr-2 size-4 animate-spin" />
        ) : (
          <Icons.google className="mr-2 size-4" />
        )}{" "}
        Google
      </button>
      
      <button
        type="button"
        className={cn(buttonVariants({ variant: "outline" }))}
        onClick={() => {
          setIsGitHubLoading(true);
          signIn("github") // This is where you specify GitHub as the provider
            .finally(() => setIsGitHubLoading(false)); // Optionally reset loading state when the sign-in promise settles
        }}
        disabled={isLoading || isGitHubLoading}
      >
        {isGitHubLoading ? (
          <Icons.spinner className="mr-2 size-4 animate-spin" />
        ) : (
          <Icons.gitHub className="mr-2 size-4" /> // Replace Icons.google with Icons.github for GitHub icon
        )}{" "}
        GitHub
      </button>

      {
        // This will conditionally render the "Login with Wallet" button only if the address is undefined
        address && (
          <button
            type="button"
            className={cn(buttonVariants({ variant: "outline" }))}
            onClick={() => {
              setIsWalletLoading(true);
              loginWithWallet().finally(() => setIsWalletLoading(false));
            }}
            disabled={isLoading || isWalletLoading}
          >
            {isWalletLoading ? (
              <Icons.spinner className="mr-2 size-4 animate-spin" />
            ) : (
              <Icons.wallet className="mr-2 size-4" />
            )}
            {" "}
            Login with Wallet
          </button>
        )
      }

      <ConnectWallet
            theme={"dark"}
            modalSize={"wide"}
          />


    </div>
  )
}

package.json

...
"dependencies": {
    "@auth/prisma-adapter": "^1.5.0",
    "@hookform/resolvers": "^3.3.4",
    "@prisma/client": "^5.11.0",
    "@radix-ui/react-accessible-icon": "^1.0.3",
    "@radix-ui/react-accordion": "^1.1.2",
    "@radix-ui/react-alert-dialog": "^1.0.5",
    "@radix-ui/react-aspect-ratio": "^1.0.3",
    "@radix-ui/react-avatar": "^1.0.4",
    "@radix-ui/react-checkbox": "^1.0.4",
    "@radix-ui/react-collapsible": "^1.0.3",
    "@radix-ui/react-context-menu": "^2.1.5",
    "@radix-ui/react-dialog": "^1.0.5",
    "@radix-ui/react-dropdown-menu": "^2.0.6",
    "@radix-ui/react-hover-card": "^1.0.7",
    "@radix-ui/react-label": "^2.0.2",
    "@radix-ui/react-menubar": "^1.0.4",
    "@radix-ui/react-navigation-menu": "^1.1.4",
    "@radix-ui/react-popover": "^1.0.7",
    "@radix-ui/react-progress": "^1.0.3",
    "@radix-ui/react-radio-group": "^1.1.3",
    "@radix-ui/react-scroll-area": "^1.0.5",
    "@radix-ui/react-select": "^2.0.0",
    "@radix-ui/react-separator": "^1.0.3",
    "@radix-ui/react-slider": "^1.1.2",
    "@radix-ui/react-slot": "^1.0.2",
    "@radix-ui/react-switch": "^1.0.3",
    "@radix-ui/react-tabs": "^1.0.4",
    "@radix-ui/react-toast": "^1.1.5",
    "@radix-ui/react-toggle": "^1.0.3",
    "@radix-ui/react-toggle-group": "^1.0.4",
    "@radix-ui/react-tooltip": "^1.0.7",
    "@react-email/button": "0.0.14",
    "@react-email/components": "0.0.15",
    "@react-email/html": "0.0.7",
    "@t3-oss/env-nextjs": "^0.9.2",
    "@typescript-eslint/parser": "^7.2.0",
    "@vercel/analytics": "^1.2.2",
    "@vercel/og": "^0.6.2",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.0",
    "cmdk": "^1.0.0",
    "concurrently": "^8.2.2",
    "contentlayer": "^0.3.4",
    "date-fns": "^3.5.0",
    "lucide-react": "^0.358.0",
    "ms": "^2.1.3",
    "next": "14.1.3",
    "next-auth": "5.0.0-beta.16",
    "next-contentlayer": "^0.3.4",
    "next-themes": "^0.3.0",
    "nodemailer": "^6.9.12",
    "prop-types": "^15.8.1",
    "react": "^18.2.0",
    "react-day-picker": "^8.10.0",
    "react-dom": "^18.2.0",
    "react-email": "2.1.0",
    "react-hook-form": "^7.51.1",
    "react-textarea-autosize": "^8.5.3",
    "resend": "^3.2.0",
    "sharp": "^0.33.2",
    "shiki": "^1.2.0",
    "stripe": "^14.21.0",
    "tailwind-merge": "^2.2.2",
    "tailwindcss-animate": "^1.0.7",
    "vaul": "^0.9.0",
    "zod": "^3.22.4",
    "zustand": "^4.5.2",
    "@radix-ui/react-icons": "^1.3.0",
    "@vercel/kv": "^1.0.1",
    "ai": "^3.0.12",
    "d3-scale": "^4.0.2",
    "focus-trap-react": "^10.2.3",
    "framer-motion": "^10.18.0",
    "geist": "^1.2.1",
    "nanoid": "^5.0.4",
    "openai": "^4.24.7",
    "react-intersection-observer": "^9.5.3",
    "react-markdown": "^8.0.7",
    "react-syntax-highlighter": "^15.5.0",
    "remark-gfm": "^3.0.1",
    "remark-math": "^5.1.1",
    "sonner": "^1.4.3",
    "usehooks-ts": "^2.16.0",
    "thirdweb": "beta",
    "@thirdweb-dev/auth": "^3",
    "@thirdweb-dev/react": "^3",
    "@thirdweb-dev/sdk": "^3",
    "ethers": "^5.7.2",
    "pino-pretty": "^10.3.1",
    "magic-sdk": "^28.0.1"
  },
  "devDependencies": {
    "@commitlint/cli": "^19.2.0",
    "@commitlint/config-conventional": "^19.1.0",
    "@ianvs/prettier-plugin-sort-imports": "^4.2.1",
    "@svgr/webpack": "^8.1.0",
    "@tailwindcss/line-clamp": "^0.4.4",
    "@tailwindcss/typography": "^0.5.10",
    "@types/d3-scale": "^4.0.8",
    "@types/node": "^20.11.28",
    "@types/react": "18.2.66",
    "@types/react-dom": "18.2.22",
    "@types/react-syntax-highlighter": "^15.5.11",
    "@typescript-eslint/parser": "^7.2.0",
    "autoprefixer": "^10.4.18",
    "dotenv": "^16.4.5",
    "eslint": "^8.57.0",
    "eslint-config-next": "14.1.3",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-react": "^7.34.1",
    "eslint-plugin-tailwindcss": "^3.15.1",
    "husky": "^9.0.11",
    "mdast-util-toc": "^7.0.0",
    "postcss": "^8.4.35",
    "prettier": "^3.2.5",
    "prettier-plugin-tailwindcss": "^0.5.12",
    "pretty-quick": "^4.0.0",
    "prisma": "^5.11.0",
    "rehype": "^13.0.1",
    "rehype-autolink-headings": "^7.1.0",
    "rehype-pretty-code": "^0.13.0",
    "rehype-slug": "^6.0.0",
    "remark": "^15.0.1",
    "remark-gfm": "^3.0.1",
    "tailwind-merge": "^2.2.2",
    "tailwindcss": "^3.4.1",
    "tailwindcss-animate": "^1.0.7",
    "typescript": "5.4.2",
    "unist-util-visit": "^5.0.0",
    "webpack": "^5.91.0"
  }

Let me know if this helps to pinpoint the problem. Appreciate your help!

broomva avatar Apr 02 '24 16:04 broomva

this is very helpful, thank you!

we don't yet have a next-auth integration for v5 and we are currently debating adding it, auth is becoming a much more simple, streamlined process so we're thinking that maybe we don't need to.

I have actually updated our basic "auth starter" project with nextjs here: https://github.com/thirdweb-example/thirdweb-auth-next/tree/main/src/app/jwt-cookie

Maybe it would be a good starting point to look at. I'd love to explore how to make the integration with next-auth work seamlessly while also preserving the core of the auth module of the v5 sdk being (essentially) 4 functions.

jnsdls avatar Apr 02 '24 17:04 jnsdls

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Apr 20 '24 01:04 stale[bot]