caplib
Version:
Credentialless Authentication Protocol Library for Web Applications
781 lines (777 loc) • 28 kB
JavaScript
// src/react/components/OnboardingForm.tsx
import { useState, useEffect } from "react";
import {
Box,
Button,
Flex,
FormControl,
FormLabel,
Heading,
Input,
Radio,
RadioGroup,
Select,
Stack,
Text,
VStack,
useToast,
FormErrorMessage,
Image
} from "@chakra-ui/react";
// src/react/assets/zerocat-logo.ts
var logoUrl = "";
try {
logoUrl = new URL("./zerocat.png", import.meta.url).href;
} catch (e) {
try {
if (typeof window !== "undefined") {
logoUrl = "/zerocat.png";
}
} catch (err) {
console.warn("Failed to load Zerocat logo:", err);
}
}
var zerocat_logo_default = logoUrl;
// src/react/components/OnboardingForm.tsx
import { jsx, jsxs } from "react/jsx-runtime";
function OnboardingForm({
walletAddress,
onComplete,
onBack,
theme = { primary: "blue", secondary: "teal" }
}) {
const toast = useToast();
const [loading, setLoading] = useState(false);
const [accountType, setAccountType] = useState("human");
const [errors, setErrors] = useState({});
const [availableRoles, setAvailableRoles] = useState(["User"]);
const [defaultRole, setDefaultRole] = useState("User");
const [details, setDetails] = useState({
first_name: "",
last_name: "",
entity_name: "",
email: "",
role: "User"
});
useEffect(() => {
const fetchRoles = async () => {
try {
const response = await fetch("/api/roles");
if (response.ok) {
const data = await response.json();
if (data.roles && Array.isArray(data.roles) && data.roles.length > 0) {
setAvailableRoles(data.roles);
if (data.defaultRole) {
setDefaultRole(data.defaultRole);
setDetails((prev) => ({
...prev,
role: data.defaultRole
}));
}
}
}
} catch (error) {
console.error("Failed to fetch roles:", error);
}
};
fetchRoles();
}, []);
const handleDetailsChange = (key, value) => {
setDetails((prev) => ({
...prev,
[key]: value
}));
if (errors[key]) {
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors[key];
return newErrors;
});
}
};
const validateForm = () => {
const newErrors = {};
if (accountType === "human") {
if (!details.first_name.trim()) {
newErrors.first_name = "First name is required";
}
if (!details.last_name.trim()) {
newErrors.last_name = "Last name is required";
}
} else {
if (!details.entity_name.trim()) {
newErrors.entity_name = "Entity name is required";
}
}
if (!details.email.trim()) {
newErrors.email = "Email is required";
} else if (!/\S+@\S+\.\S+/.test(details.email)) {
newErrors.email = "Email is invalid";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setLoading(true);
try {
const payload = {
wallet_address: walletAddress,
first_name: accountType === "human" ? details.first_name : "",
last_name: accountType === "human" ? details.last_name : "ENTITY",
entity_name: accountType === "entity" ? details.entity_name : "",
email: details.email,
role: details.role,
account_type: accountType
};
const createResponse = await fetch("/api/users/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!createResponse.ok) {
throw new Error("Failed to create profile");
}
const data = await createResponse.json();
onComplete(data.user);
toast({
title: "Profile created",
description: "Your profile has been successfully created.",
status: "success",
duration: 5e3,
isClosable: true
});
} catch (error) {
console.error("Registration error:", error);
toast({
title: "Registration failed",
description: error instanceof Error ? error.message : "Failed to create account",
status: "error",
duration: 5e3,
isClosable: true
});
} finally {
setLoading(false);
}
};
return /* @__PURE__ */ jsxs(Box, { width: "100%", p: 6, position: "relative", children: [
/* @__PURE__ */ jsxs(VStack, { spacing: 6, align: "stretch", children: [
/* @__PURE__ */ jsxs(Box, { textAlign: "center", mb: 6, children: [
/* @__PURE__ */ jsx(Flex, { justifyContent: "center", mb: 4, children: /* @__PURE__ */ jsx(
Image,
{
src: zerocat_logo_default,
alt: "ZEROCAT",
height: "32px",
width: "80px",
objectFit: "contain"
}
) }),
/* @__PURE__ */ jsx(
Heading,
{
as: "h1",
size: "xl",
bgGradient: `linear(to-r, ${theme.primary}.400, ${theme.secondary}.400)`,
backgroundClip: "text",
mb: 2,
children: "Complete Your Profile"
}
),
/* @__PURE__ */ jsx(Text, { color: "whiteAlpha.700", children: "Tell us a bit about yourself" })
] }),
/* @__PURE__ */ jsx("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxs(VStack, { spacing: 6, align: "stretch", children: [
/* @__PURE__ */ jsxs(FormControl, { as: "fieldset", children: [
/* @__PURE__ */ jsx(FormLabel, { as: "legend", fontWeight: "medium", children: "Account Type" }),
/* @__PURE__ */ jsx(
RadioGroup,
{
defaultValue: "human",
onChange: (value) => setAccountType(value),
value: accountType,
children: /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 5, children: [
/* @__PURE__ */ jsx(Radio, { value: "human", children: "Individual" }),
/* @__PURE__ */ jsx(Radio, { value: "entity", children: "Entity/Organization" })
] })
}
)
] }),
accountType === "human" ? /* @__PURE__ */ jsxs(Stack, { direction: { base: "column", md: "row" }, spacing: 4, children: [
/* @__PURE__ */ jsxs(FormControl, { isInvalid: !!errors.first_name, children: [
/* @__PURE__ */ jsx(FormLabel, { children: "First Name" }),
/* @__PURE__ */ jsx(
Input,
{
value: details.first_name,
onChange: (e) => handleDetailsChange("first_name", e.target.value),
placeholder: "John"
}
),
/* @__PURE__ */ jsx(FormErrorMessage, { children: errors.first_name })
] }),
/* @__PURE__ */ jsxs(FormControl, { isInvalid: !!errors.last_name, children: [
/* @__PURE__ */ jsx(FormLabel, { children: "Last Name" }),
/* @__PURE__ */ jsx(
Input,
{
value: details.last_name,
onChange: (e) => handleDetailsChange("last_name", e.target.value),
placeholder: "Doe"
}
),
/* @__PURE__ */ jsx(FormErrorMessage, { children: errors.last_name })
] })
] }) : /* @__PURE__ */ jsxs(FormControl, { isInvalid: !!errors.entity_name, children: [
/* @__PURE__ */ jsx(FormLabel, { children: "Organization Name" }),
/* @__PURE__ */ jsx(
Input,
{
value: details.entity_name,
onChange: (e) => handleDetailsChange("entity_name", e.target.value),
placeholder: "Company or Organization Name"
}
),
/* @__PURE__ */ jsx(FormErrorMessage, { children: errors.entity_name })
] }),
/* @__PURE__ */ jsxs(FormControl, { isInvalid: !!errors.email, children: [
/* @__PURE__ */ jsx(FormLabel, { children: "Email" }),
/* @__PURE__ */ jsx(
Input,
{
type: "email",
value: details.email,
onChange: (e) => handleDetailsChange("email", e.target.value),
placeholder: "you@example.com"
}
),
/* @__PURE__ */ jsx(FormErrorMessage, { children: errors.email })
] }),
/* @__PURE__ */ jsxs(FormControl, { children: [
/* @__PURE__ */ jsx(FormLabel, { children: "Role" }),
/* @__PURE__ */ jsx(
Select,
{
value: details.role,
onChange: (e) => handleDetailsChange("role", e.target.value),
children: availableRoles.map((role) => /* @__PURE__ */ jsx("option", { value: role, children: role }, role))
}
)
] }),
/* @__PURE__ */ jsxs(Flex, { gap: 3, pt: 4, children: [
/* @__PURE__ */ jsx(
Button,
{
flex: "1",
variant: "outline",
onClick: onBack,
isDisabled: loading,
children: "Back"
}
),
/* @__PURE__ */ jsx(
Button,
{
flex: "1",
variant: "gradient",
type: "submit",
isLoading: loading,
loadingText: "Creating...",
children: "Complete"
}
)
] })
] }) })
] }),
/* @__PURE__ */ jsxs(
Flex,
{
justifyContent: "center",
alignItems: "center",
position: "absolute",
bottom: "0",
left: "0",
right: "0",
p: 2,
borderTop: "1px",
borderColor: "whiteAlpha.100",
bg: "whiteAlpha.50",
children: [
/* @__PURE__ */ jsx(Text, { fontSize: "xs", color: "whiteAlpha.600", mr: 1, children: "Powered by" }),
/* @__PURE__ */ jsx(
Image,
{
src: zerocat_logo_default,
alt: "ZEROCAT",
height: "12px",
width: "28px"
}
)
]
}
)
] });
}
// src/react/components/CapAuth.tsx
import { useState as useState2, useEffect as useEffect2, useRef } from "react";
import { QRCodeSVG } from "qrcode.react";
import { AnimatePresence, motion } from "framer-motion";
import {
Box as Box2,
Button as Button2,
Center,
Flex as Flex2,
Heading as Heading2,
Icon,
Image as Image2,
Spinner,
Step,
StepIcon,
StepIndicator,
StepNumber,
StepSeparator,
StepStatus,
StepTitle,
Stepper,
Text as Text2,
useSteps,
VStack as VStack2,
useToast as useToast2
} from "@chakra-ui/react";
import { PhoneIcon, CheckIcon, RepeatIcon } from "@chakra-ui/icons";
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
var fadeIn = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 0.3 } },
exit: { opacity: 0, transition: { duration: 0.2 } }
};
var slideRight = {
hidden: { x: -30, opacity: 0 },
visible: { x: 0, opacity: 1, transition: { duration: 0.4 } },
exit: { x: 30, opacity: 0, transition: { duration: 0.3 } }
};
var steps = [
{ title: "Prepare" },
{ title: "Scan" },
{ title: "Verify" },
{ title: "Profile" }
];
function CapAuth({
onAuthenticated,
config,
onError
}) {
const toast = useToast2();
const { activeStep, setActiveStep } = useSteps({
index: 0,
count: steps.length
});
const [stage, setStage] = useState2("intro");
const [qrData, setQrData] = useState2(null);
const [qrUri, setQrUri] = useState2(null);
const [error, setError] = useState2(null);
const [nonce, setNonce] = useState2(null);
const [walletAddress, setWalletAddress] = useState2(null);
const [isPollingActive, setIsPollingActive] = useState2(true);
const pollingIntervalRef = useRef(null);
const timeoutIdRef = useRef(null);
const isAuthenticatedRef = useRef(false);
const cleanupTimers = () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = null;
}
};
useEffect2(() => {
return () => cleanupTimers();
}, []);
useEffect2(() => {
if (stage === "polling" && nonce && isPollingActive && !isAuthenticatedRef.current) {
const interval = setInterval(async () => {
try {
const response = await fetch("/api/auth", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nonce })
});
if (!response.ok) throw new Error("Verification failed");
const data = await response.json();
if (data.authenticated && data.userAddress) {
isAuthenticatedRef.current = true;
setIsPollingActive(false);
cleanupTimers();
if (data.user) {
onAuthenticated(data.user);
toast({
title: "Authentication successful",
status: "success",
duration: 3e3,
isClosable: true
});
} else {
setActiveStep(3);
setWalletAddress(data.userAddress);
setStage("onboarding");
}
}
} catch (error2) {
console.error("Polling error:", error2);
}
}, config.timeouts?.polling || 3e3);
pollingIntervalRef.current = interval;
const timeout = setTimeout(() => {
cleanupTimers();
setError("Authentication timeout. Please try again.");
setStage("intro");
setActiveStep(0);
}, config.timeouts?.authentication || 3e5);
timeoutIdRef.current = timeout;
return () => cleanupTimers();
}
}, [stage, nonce, isPollingActive, config, onAuthenticated, toast, setActiveStep]);
const generateQR = async () => {
try {
setError(null);
setActiveStep(1);
const response = await fetch("/api/auth");
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "Failed to generate authentication code");
}
const receiverAddr = process.env.NEXT_PUBLIC_SERVER_WALLET_ADDRESS || "";
const contractId = process.env.NEXT_PUBLIC_AUTH_CONTRACT_ID || "";
const tokenId = process.env.NEXT_PUBLIC_TOKEN_ID || "";
const txDetails = `smart_contract_auth_${contractId},nonce_${data.nonce}`;
const txParams = `amount=0,recipientAddress=${receiverAddr},senderAddress=customer_,timestamp=${(/* @__PURE__ */ new Date()).toISOString()},tokenId=${tokenId},transactionCost=0,transactionDetails=${txDetails},transactionId=transaction_`;
setQrData(txParams);
setQrUri(`zerowallet://transaction?${txParams}`);
setNonce(data.nonce);
setStage("qr");
} catch (error2) {
console.error("QR generation error:", error2);
setError("Failed to generate authentication code. Please try again.");
toast({
title: "Error",
description: "Failed to generate authentication code. Please try again.",
status: "error",
duration: 5e3,
isClosable: true
});
if (onError && error2 instanceof Error) {
onError(error2);
}
}
};
const handleOpenMobileWallet = () => {
if (config.enableMobileWallet && qrUri) {
window.location.href = qrUri;
}
};
const proceedToVerification = () => {
setActiveStep(2);
setStage("polling");
setIsPollingActive(true);
};
const handleRetry = () => {
cleanupTimers();
setError(null);
setQrData(null);
setQrUri(null);
setNonce(null);
setWalletAddress(null);
setStage("intro");
setActiveStep(0);
setIsPollingActive(true);
isAuthenticatedRef.current = false;
};
const handleOnboardingComplete = (user) => {
setIsPollingActive(false);
isAuthenticatedRef.current = true;
if (walletAddress) {
onAuthenticated(user);
toast({
title: "Profile completed",
description: "You're now successfully signed in.",
status: "success",
duration: 3e3,
isClosable: true
});
}
};
const {
theme = { primary: "blue", secondary: "teal" }
} = config;
return /* @__PURE__ */ jsxs2(
Box2,
{
width: { base: "90vw", sm: "100%" },
maxW: "md",
borderRadius: "xl",
overflow: "hidden",
bg: "dark.200",
borderWidth: "1px",
borderColor: "whiteAlpha.200",
boxShadow: "xl",
position: "relative",
children: [
/* @__PURE__ */ jsx2(Box2, { p: { base: 2, sm: 4 }, bg: "whiteAlpha.50", children: /* @__PURE__ */ jsx2(Stepper, { index: activeStep, colorScheme: "brand", size: "sm", children: steps.map((step, index) => /* @__PURE__ */ jsxs2(Step, { children: [
/* @__PURE__ */ jsx2(StepIndicator, { children: /* @__PURE__ */ jsx2(
StepStatus,
{
complete: /* @__PURE__ */ jsx2(StepIcon, {}),
incomplete: /* @__PURE__ */ jsx2(StepNumber, {}),
active: /* @__PURE__ */ jsx2(StepNumber, {})
}
) }),
/* @__PURE__ */ jsx2(Box2, { flexShrink: 0, children: /* @__PURE__ */ jsx2(StepTitle, { fontSize: { base: "xs", sm: "sm" }, children: step.title }) }),
/* @__PURE__ */ jsx2(StepSeparator, {})
] }, index)) }) }),
/* @__PURE__ */ jsxs2(AnimatePresence, { mode: "wait", children: [
stage === "intro" && /* @__PURE__ */ jsx2(
motion.div,
{
variants: fadeIn,
initial: "hidden",
animate: "visible",
exit: "exit",
children: /* @__PURE__ */ jsxs2(VStack2, { spacing: { base: 4, sm: 6 }, p: { base: 4, sm: 6 }, children: [
/* @__PURE__ */ jsxs2(Box2, { textAlign: "center", children: [
/* @__PURE__ */ jsx2(
Heading2,
{
as: "h1",
size: { base: "lg", sm: "xl" },
bgGradient: `linear(to-r, ${theme.primary}.400, ${theme.secondary}.400)`,
backgroundClip: "text",
mb: 2,
children: config.appName
}
),
/* @__PURE__ */ jsx2(Text2, { color: "whiteAlpha.700", fontSize: { base: "sm", sm: "md" }, children: config.appDescription })
] }),
/* @__PURE__ */ jsxs2(VStack2, { spacing: 4, w: "full", children: [
/* @__PURE__ */ jsxs2(
Flex2,
{
align: "flex-start",
gap: 4,
p: 4,
bg: "whiteAlpha.50",
borderRadius: "lg",
w: "full",
children: [
/* @__PURE__ */ jsx2(Box2, { color: "blue.400", mt: 1, children: /* @__PURE__ */ jsx2(PhoneIcon, { boxSize: { base: 4, sm: 5 } }) }),
/* @__PURE__ */ jsxs2(Box2, { children: [
/* @__PURE__ */ jsx2(Text2, { fontWeight: "medium", fontSize: { base: "sm", sm: "md" }, children: "Get Your Wallet Ready" }),
/* @__PURE__ */ jsx2(Text2, { fontSize: { base: "xs", sm: "sm" }, color: "whiteAlpha.700", children: "Ensure you have ZeroWallet installed and ready" })
] })
]
}
),
/* @__PURE__ */ jsxs2(
Flex2,
{
align: "flex-start",
gap: 4,
p: 4,
bg: "whiteAlpha.50",
borderRadius: "lg",
w: "full",
children: [
/* @__PURE__ */ jsx2(Box2, { color: "teal.400", mt: 1, children: /* @__PURE__ */ jsx2(
Icon,
{
viewBox: "0 0 24 24",
boxSize: { base: 4, sm: 5 },
children: /* @__PURE__ */ jsx2(
"path",
{
fill: "currentColor",
d: "M3 11h8V3H3v8zm2-6h4v4H5V5zm8-2v8h8V3h-8zm6 6h-4V5h4v4zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm13-2h-2v4h-4v2h4v4h2v-4h4v-2h-4v-4z"
}
)
}
) }),
/* @__PURE__ */ jsxs2(Box2, { children: [
/* @__PURE__ */ jsx2(Text2, { fontWeight: "medium", fontSize: { base: "sm", sm: "md" }, children: "Credentialless Auth" }),
/* @__PURE__ */ jsx2(Text2, { fontSize: { base: "xs", sm: "sm" }, color: "whiteAlpha.700", children: "Scan QR code with your phone to open ZeroWallet" })
] })
]
}
)
] }),
/* @__PURE__ */ jsx2(
Button2,
{
w: "full",
variant: "gradient",
size: { base: "md", sm: "lg" },
onClick: generateQR,
rightIcon: /* @__PURE__ */ jsx2(Icon, { viewBox: "0 0 24 24", boxSize: { base: 4, sm: 5 }, children: /* @__PURE__ */ jsx2(
"path",
{
fill: "currentColor",
d: "M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"
}
) }),
children: "Start Authentication"
}
)
] })
},
"intro"
),
stage === "qr" && qrData && /* @__PURE__ */ jsx2(
motion.div,
{
variants: slideRight,
initial: "hidden",
animate: "visible",
exit: "exit",
children: /* @__PURE__ */ jsxs2(VStack2, { spacing: { base: 4, sm: 6 }, p: { base: 4, sm: 6 }, children: [
/* @__PURE__ */ jsxs2(Box2, { textAlign: "center", children: [
/* @__PURE__ */ jsx2(Heading2, { as: "h2", size: { base: "md", sm: "lg" }, mb: 2, children: "Scan QR Code" }),
/* @__PURE__ */ jsx2(Text2, { color: "whiteAlpha.700", fontSize: { base: "xs", sm: "sm" }, children: "Open your camera app and scan this code" })
] }),
/* @__PURE__ */ jsx2(Center, { py: { base: 4, sm: 6 }, children: /* @__PURE__ */ jsx2(Box2, { p: { base: 3, sm: 4 }, bg: "white", borderRadius: "xl", boxShadow: "xl", children: /* @__PURE__ */ jsx2(
QRCodeSVG,
{
value: qrUri || "",
size: Math.min(220, window.innerWidth * 0.6),
level: "M",
includeMargin: true
}
) }) }),
/* @__PURE__ */ jsxs2(VStack2, { spacing: 4, w: "full", children: [
config.enableMobileWallet && /* @__PURE__ */ jsx2(
Button2,
{
w: "full",
variant: "outline",
onClick: handleOpenMobileWallet,
leftIcon: /* @__PURE__ */ jsx2(PhoneIcon, {}),
size: { base: "sm", sm: "md" },
children: "Open in ZeroWallet"
}
),
/* @__PURE__ */ jsx2(
Button2,
{
w: "full",
variant: "gradient",
onClick: proceedToVerification,
rightIcon: /* @__PURE__ */ jsx2(CheckIcon, {}),
size: { base: "sm", sm: "md" },
children: "I've Completed the Transaction"
}
)
] })
] })
},
"qr"
),
stage === "polling" && /* @__PURE__ */ jsx2(
motion.div,
{
variants: fadeIn,
initial: "hidden",
animate: "visible",
exit: "exit",
children: /* @__PURE__ */ jsxs2(VStack2, { spacing: 6, p: { base: 6, sm: 8 }, textAlign: "center", children: [
/* @__PURE__ */ jsx2(
Spinner,
{
thickness: "4px",
speed: "0.65s",
emptyColor: "whiteAlpha.200",
color: "brand.500",
size: { base: "lg", sm: "xl" }
}
),
/* @__PURE__ */ jsxs2(Box2, { children: [
/* @__PURE__ */ jsx2(Heading2, { as: "h2", size: { base: "md", sm: "lg" }, mb: 2, children: "Verifying" }),
/* @__PURE__ */ jsx2(Text2, { color: "whiteAlpha.700", fontSize: { base: "sm", sm: "md" }, children: "Please wait while we confirm your transaction..." })
] })
] })
},
"polling"
),
stage === "onboarding" && walletAddress && /* @__PURE__ */ jsx2(
motion.div,
{
variants: slideRight,
initial: "hidden",
animate: "visible",
exit: "exit",
children: /* @__PURE__ */ jsx2(
OnboardingForm,
{
walletAddress,
onComplete: handleOnboardingComplete,
onBack: handleRetry,
theme
}
)
},
"onboarding"
)
] }),
error && /* @__PURE__ */ jsx2(
motion.div,
{
initial: { opacity: 0, y: 10 },
animate: { opacity: 1, y: 0 },
children: /* @__PURE__ */ jsx2(Box2, { p: { base: 3, sm: 4 }, bg: "red.900", color: "white", mt: 2, children: /* @__PURE__ */ jsxs2(VStack2, { spacing: { base: 3, sm: 4 }, children: [
/* @__PURE__ */ jsx2(Text2, { color: "red.200", textAlign: "center", fontSize: { base: "sm", sm: "md" }, children: error }),
/* @__PURE__ */ jsx2(
Button2,
{
leftIcon: /* @__PURE__ */ jsx2(RepeatIcon, {}),
colorScheme: "red",
variant: "outline",
size: "sm",
onClick: handleRetry,
children: "Try Again"
}
)
] }) })
}
),
/* @__PURE__ */ jsxs2(
Flex2,
{
justifyContent: "center",
alignItems: "center",
p: 2,
borderTop: "1px",
borderColor: "whiteAlpha.100",
bg: "whiteAlpha.50",
children: [
/* @__PURE__ */ jsx2(Text2, { fontSize: "xs", color: "whiteAlpha.600", mr: 1, children: "Powered by" }),
/* @__PURE__ */ jsx2(
Image2,
{
src: zerocat_logo_default,
alt: "ZEROCAT",
height: { base: "10px", sm: "12px" },
width: { base: "24px", sm: "28px" }
}
)
]
}
)
]
}
);
}
export {
OnboardingForm,
CapAuth
};
//# sourceMappingURL=chunk-RYE5TPNM.mjs.map