diff --git a/mintro-mini-app/.env.sample b/mintro-mini-app/.env.sample index fa2a79a..a706aed 100644 --- a/mintro-mini-app/.env.sample +++ b/mintro-mini-app/.env.sample @@ -5,5 +5,12 @@ AUTH_URL= WLD_CLIENT_ID= NEXT_PUBLIC_WLD_CLIENT_ID= NEXT_PUBLIC_PRIVY_APP_ID= -NEXT_PUBLIC_RPC_URL +NEXT_PUBLIC_RPC_URL= +NEXT_PUBLIC_COINBASE_SECRET_KEY= +NEXT_PUBLIC_COINBASE_PUBLIC_KEY= +NEXT_PUBLIC_COINBASE_APP_ID= +COINBASE_API_KEY_NAME= + + + diff --git a/mintro-mini-app/README.md b/mintro-mini-app/README.md index d05d5b9..1ac5f84 100644 --- a/mintro-mini-app/README.md +++ b/mintro-mini-app/README.md @@ -21,6 +21,24 @@ This template is a way for you to quickly get started with authentication and ex 9. Continue to developer.worldcoin.org and make sure your app is connected to the right ngrok url 10. [Optional] For Verify and Send Transaction to work you need to do some more setup in the dev portal. The steps are outlined in the respective component files. +## Add Money Quick Action + +This mini app now uses the World App's Add Money Quick Action instead of Coinbase Onramp. The "Fund Wallet" button will open the World App's bridge interface where users can: + +- Add money to their World Wallet directly from exchanges like Binance and Coinbase +- Deposit, withdraw, and swap tokens across multiple exchanges and chains +- Support for USDC and WLD tokens + +The integration uses the following parameters: + +- `app_id`: Your World App mini app ID +- `path`: URL-encoded path to the bridge interface (`%2Fbridge`) +- `toAddress`: The user's World Wallet address +- `toToken`: Token contract address (WLD: `0x16345785d8a0000`) +- `sourceAppId`: Your app ID for navigation back +- `sourceAppName`: "Mintro" (your app name) +- `sourceDeeplinkPath`: URL-encoded path back to your app (`%2Fhome`) + ### Environment Variables Create a `.env.local` file with the following variables: diff --git a/mintro-mini-app/next.config.ts b/mintro-mini-app/next.config.ts index f520a86..a1311d1 100644 --- a/mintro-mini-app/next.config.ts +++ b/mintro-mini-app/next.config.ts @@ -4,7 +4,10 @@ const nextConfig: NextConfig = { images: { domains: ["static.usernames.app-backend.toolsforhumanity.com"], }, - allowedDevOrigins: ["https://mintro-two.vercel.app"], // Add your production origin here + allowedDevOrigins: [ + "https://mintro-two.vercel.app", + "https://5758-83-144-23-154.ngrok-free.app", + ], // Add your production and dev origins here reactStrictMode: false, }; diff --git a/mintro-mini-app/src/app/(protected)/home/page.tsx b/mintro-mini-app/src/app/(protected)/home/page.tsx index ee62686..48bf20a 100644 --- a/mintro-mini-app/src/app/(protected)/home/page.tsx +++ b/mintro-mini-app/src/app/(protected)/home/page.tsx @@ -1,30 +1,36 @@ "use client"; - -import { useWorldcoinAuth } from "@/hooks/useWorldcoinAuth"; import { Page } from "@/components/PageLayout"; import { LogoutButton } from "@/components/LogoutButton"; import { Pay } from "@/components/Pay"; import { Transaction } from "@/components/Transaction"; import { UserInfo } from "@/components/UserInfo"; -import { Verify } from "@/components/Verify"; +// import { Verify } from "@/components/Verify"; import { ViewPermissions } from "@/components/ViewPermissions"; -import { WalletBalance } from "@/components/WalletBalance"; +// import { WalletBalance } from "@/components/WalletBalance"; import { Marble, TopBar } from "@worldcoin/mini-apps-ui-kit-react"; +import { useWorldcoinAuth } from "@/hooks/useWorldcoinAuth"; +import { useCallback } from "react"; -export default function Home() { - const { user, isLoading } = useWorldcoinAuth(); +export default function ProtectedHome() { + const { user } = useWorldcoinAuth(); - if (isLoading) { - return ( - - -
-

Loading...

-
-
-
- ); - } + const fundWallet = useCallback(async () => { + if (!user?.address) return; + try { + const response = await fetch("/api/onramp", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + address: user.address, + amountUsd: "100", // Optional: you can make this configurable + }), + }); + const { url } = await response.json(); + window.open(url, "_blank"); + } catch (e) { + console.error(e); + } + }, [user]); return ( <> @@ -42,9 +48,15 @@ export default function Home() { /> + - - + {/* */} + {/* */} diff --git a/mintro-mini-app/src/app/api/onramp/route.ts b/mintro-mini-app/src/app/api/onramp/route.ts new file mode 100644 index 0000000..4ee3c5b --- /dev/null +++ b/mintro-mini-app/src/app/api/onramp/route.ts @@ -0,0 +1,58 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(req: NextRequest) { + try { + const { address, amountUsd } = await req.json(); + + if (!address) { + return NextResponse.json( + { error: "Address is required" }, + { status: 400 } + ); + } + + // World App Add Money Quick Action parameters + const appId = "app_e7d27c5ce2234e00558776f227f791ef"; + const path = "/"; // URL-encoded path to the bridge interface + const toAddress = address; + const toToken = "0x79A02482A880bCE3F13e09Da970dC34db4CD24d1"; // UDSC token contract address on World Chain + const sourceAppId = process.env.NEXT_PUBLIC_WLD_CLIENT_ID; + const sourceAppName = "Mintro"; // Your app name + const sourceDeeplinkPath = "/home"; // URL-encoded path back to your app + + // Build the World App Add Money URL + const baseUrl = "https://worldcoin.org/mini-app"; + const params = new URLSearchParams({ + app_id: appId || "", + path, + toAddress, + toToken, + sourceAppId: sourceAppId || "", + sourceAppName, + sourceDeeplinkPath, + }); + + // Add amount if provided + if (amountUsd) { + params.append("amountUsd", amountUsd.toString()); + } + + const addMoneyUrl = `${baseUrl}?${params.toString()}`; + + console.log("=== WORLD APP ADD MONEY URL ==="); + console.log("App ID:", appId); + console.log("To Address:", toAddress); + console.log("To Token:", toToken); + console.log("Amount USD:", amountUsd); + console.log("Full URL:", addMoneyUrl); + console.log("=== END WORLD APP ADD MONEY URL ==="); + + return NextResponse.json({ url: addMoneyUrl }); + } catch (error) { + console.error("Error creating World App Add Money URL:", error); + return NextResponse.json( + { error: "Failed to create add money URL" }, + { status: 500 } + ); + } +} diff --git a/mintro-mini-app/src/app/page.tsx b/mintro-mini-app/src/app/page.tsx index 8a95e32..9faca80 100644 --- a/mintro-mini-app/src/app/page.tsx +++ b/mintro-mini-app/src/app/page.tsx @@ -1,27 +1,96 @@ +"use client"; +import { useWorldcoinAuth } from "@/hooks/useWorldcoinAuth"; import { Page } from "@/components/PageLayout"; -import { AuthButton } from "../components/AuthButton"; -import { AuthStatus } from "../components/AuthStatus"; -import { MintroBranding } from "../components/MintroBranding"; -import { DebugInfo } from "../components/DebugInfo"; +import { LogoutButton } from "@/components/LogoutButton"; +// import { Pay } from "@/components/Pay"; +// import { Transaction } from "@/components/Transaction"; +import { UserInfo } from "@/components/UserInfo"; +// import { Verify } from "@/components/Verify"; +// import { ViewPermissions } from "@/components/ViewPermissions"; +// import { WalletBalance } from "@/components/WalletBalance"; +import { Marble, TopBar } from "@worldcoin/mini-apps-ui-kit-react"; +import { AuthButton } from "@/components/AuthButton"; +import { useCallback } from "react"; export default function Home() { - return ( - - - {/* Mintro Branding and Timeline */} - + const { user, isLoading, isAuthenticated } = useWorldcoinAuth(); + + const fundWallet = useCallback(async () => { + if (!user?.address) return; + try { + const response = await fetch("/api/onramp", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + address: user.address, + amountUsd: "100", // Optional: you can make this configurable + }), + }); + const { url } = await response.json(); + window.open(url, "_blank"); + } catch (e) { + console.error(e); + } + }, [user]); + + if (isLoading) { + return ( + + +
+

Loading...

+
+
+
+ ); + } - {/* Authentication Section */} -
- -
- + if (!isAuthenticated) { + return ( + + +
+
+ +
-
+ + + ); + } + + return ( + <> + + +

+ {user?.username || "User"} +

+ +
+ } + /> + + + + - {/* Debug Information - Always at the bottom */} - + {/* */} + {/* */} + {/* */} + {/* */} + {/* + */} + -
+ ); } diff --git a/mintro-mini-app/src/components/AuthButton/index.tsx b/mintro-mini-app/src/components/AuthButton/index.tsx index 2c95eb9..717ed77 100644 --- a/mintro-mini-app/src/components/AuthButton/index.tsx +++ b/mintro-mini-app/src/components/AuthButton/index.tsx @@ -79,9 +79,8 @@ export const AuthButton = () => { payload: result.finalPayload, }) ); - - // Redirect to home page - router.push("/home"); + window.dispatchEvent(new Event("worldcoin_auth_update")); + // router.push("/protected/home"); } catch (error) { console.error("Worldcoin wallet authentication error", error); } finally { diff --git a/mintro-mini-app/src/components/LogoutButton/index.tsx b/mintro-mini-app/src/components/LogoutButton/index.tsx index 6ce1940..8d531e1 100644 --- a/mintro-mini-app/src/components/LogoutButton/index.tsx +++ b/mintro-mini-app/src/components/LogoutButton/index.tsx @@ -2,16 +2,14 @@ import { Button } from "@worldcoin/mini-apps-ui-kit-react"; import { useWorldcoinAuth } from "@/hooks/useWorldcoinAuth"; -import { useRouter } from "next/navigation"; export const LogoutButton = () => { const { logout } = useWorldcoinAuth(); - const router = useRouter(); const handleLogout = async () => { try { logout(); - router.push("/"); + window.location.reload(); // Soft reload, resets all state } catch (error) { console.error("Logout error:", error); } diff --git a/mintro-mini-app/src/components/MintroBranding/index.tsx b/mintro-mini-app/src/components/MintroBranding/index.tsx index 043511d..ff8e477 100644 --- a/mintro-mini-app/src/components/MintroBranding/index.tsx +++ b/mintro-mini-app/src/components/MintroBranding/index.tsx @@ -3,7 +3,7 @@ import { useWorldcoinAuth } from "@/hooks/useWorldcoinAuth"; import { Button } from "@worldcoin/mini-apps-ui-kit-react"; import { useState, useEffect } from "react"; -import { ethers } from "ethers"; +// Removed: import { ethers } from "ethers"; interface SwapNotification { id: string; @@ -87,20 +87,12 @@ const getTypeIcon = (type: string) => { } }; -const WLD_CONTRACT = "0x163f8C2467924be0ae7B5347228C0F3Fc0cC008e"; // WLD token contract on World Chain - needs checksum -const ERC20_ABI = [ - "function balanceOf(address owner) view returns (uint256)", - "function decimals() view returns (uint8)", -]; - export const MintroBranding = () => { const { isLoading, isAuthenticated, user } = useWorldcoinAuth(); const [formattedNotifications, setFormattedNotifications] = useState< SwapNotification[] >([]); const [isClient, setIsClient] = useState(false); - const [wldBalance, setWldBalance] = useState(null); - const [balanceError, setBalanceError] = useState(null); useEffect(() => { setIsClient(true); @@ -113,62 +105,6 @@ export const MintroBranding = () => { setFormattedNotifications(formatted); }, []); - useEffect(() => { - async function fetchWldBalance() { - if (!user?.address) { - console.log("No user address available"); - return; - } - - try { - console.log("Fetching WLD balance for address:", user.address); - - // Check if RPC URL is available - const rpcUrl = process.env.NEXT_PUBLIC_RPC_URL; - if (!rpcUrl) { - throw new Error( - "NEXT_PUBLIC_RPC_URL environment variable is not set" - ); - } - - console.log("Using RPC URL:", rpcUrl); - console.log("Network: World Chain (Worldcoin's blockchain)"); - - const provider = new ethers.JsonRpcProvider(rpcUrl); - const checksummedAddress = ethers.getAddress(WLD_CONTRACT); - console.log("Contract address:", WLD_CONTRACT); - console.log("Checksummed address:", checksummedAddress); - const contract = new ethers.Contract( - checksummedAddress, - ERC20_ABI, - provider - ); - - const [rawBalance, decimals] = await Promise.all([ - contract.balanceOf(user.address), - contract.decimals(), - ]); - - console.log("Raw balance:", rawBalance.toString()); - console.log("Decimals:", decimals); - - const formattedBalance = ethers.formatUnits(rawBalance, decimals); - console.log("Formatted balance:", formattedBalance); - - setWldBalance(formattedBalance); - setBalanceError(null); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - console.error("Error fetching WLD balance:", errorMessage); - setBalanceError(errorMessage); - setWldBalance(null); - } - } - - fetchWldBalance(); - }, [user?.address]); - return (
{/* Hero Section */} @@ -178,7 +114,7 @@ export const MintroBranding = () => { Mintro

- Your intelligent DeFi companion v0.13 + Your intelligent DeFi companion v0.17

@@ -242,16 +178,6 @@ export const MintroBranding = () => { ) : isAuthenticated ? (
- {wldBalance !== null && ( -
- WLD Balance: {wldBalance} -
- )} - {balanceError && ( -
- Error fetching balance: {balanceError} -
- )}
User Address: {user?.address}
diff --git a/mintro-mini-app/src/hooks/useWorldcoinAuth.ts b/mintro-mini-app/src/hooks/useWorldcoinAuth.ts index 02f5dc2..af80b08 100644 --- a/mintro-mini-app/src/hooks/useWorldcoinAuth.ts +++ b/mintro-mini-app/src/hooks/useWorldcoinAuth.ts @@ -46,6 +46,14 @@ export const useWorldcoinAuth = () => { }; loadAuthData(); + + const handleAuthUpdate = () => { + loadAuthData(); + }; + window.addEventListener("worldcoin_auth_update", handleAuthUpdate); + return () => { + window.removeEventListener("worldcoin_auth_update", handleAuthUpdate); + }; }, []); const loadUserInfo = async (address: string) => { diff --git a/mintro-mini-app/yarn.lock b/mintro-mini-app/yarn.lock index 41529e9..fb9b82a 100644 --- a/mintro-mini-app/yarn.lock +++ b/mintro-mini-app/yarn.lock @@ -2986,6 +2986,11 @@ bs58@^5.0.0: dependencies: base-x "^4.0.0" +buffer-equal-constant-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer@6.0.3, buffer@^6.0.3, buffer@~6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" @@ -3390,6 +3395,13 @@ duplexify@^4.1.2: readable-stream "^3.1.1" stream-shift "^1.0.2" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + elliptic@6.6.1: version "6.6.1" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.1.tgz#3b8ffb02670bf69e382c7f65bf524c97c5405c06" @@ -4607,6 +4619,22 @@ json5@^1.0.2: dependencies: minimist "^1.2.0" +jsonwebtoken@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: version "3.3.5" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" @@ -4617,6 +4645,23 @@ json5@^1.0.2: object.assign "^4.1.4" object.values "^1.1.6" +jwa@^1.4.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9" + integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw== + dependencies: + buffer-equal-constant-time "^1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -4786,11 +4831,46 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + lokijs@^1.5.12: version "1.5.12" resolved "https://registry.yarnpkg.com/lokijs/-/lokijs-1.5.12.tgz#cb55b37009bdf09ee7952a6adddd555b893653a0" @@ -5691,7 +5771,7 @@ secure-password-utilities@^0.2.1: resolved "https://registry.yarnpkg.com/secure-password-utilities/-/secure-password-utilities-0.2.1.tgz#14a0d0c17c26ace573f5e5383df4cc2b51c27479" integrity sha512-znUg8ae3cpuAaogiFBhP82gD2daVkSz4Qv/L7OWjB7wWvfbCdeqqQuJkm2/IvhKQPOV0T739YPR6rb7vs0uWaw== -semver@7.7.2, semver@^7.3.8, semver@^7.6.0, semver@^7.6.3, semver@^7.7.1: +semver@7.7.2, semver@^7.3.8, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3, semver@^7.7.1: version "7.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== diff --git a/package.json b/package.json new file mode 100644 index 0000000..2b60028 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "@types/jsonwebtoken": "^9.0.10", + "jsonwebtoken": "^9.0.2" + } +}