UNPKG

caplib

Version:

Credentialless Authentication Protocol Library for Web Applications

1,392 lines (1,379 loc) 51.3 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { AuthProvider: () => AuthProvider, createAuthHandler: () => createAuthHandler, useAuth: () => useAuth, withAuth: () => withAuth }); module.exports = __toCommonJS(index_exports); // src/react/components/AuthProvider.tsx var import_react6 = require("react"); var import_react7 = require("@chakra-ui/react"); // src/react/components/CapAuth.tsx var import_react3 = require("react"); var import_qrcode = require("qrcode.react"); var import_framer_motion = require("framer-motion"); var import_react4 = require("@chakra-ui/react"); var import_icons = require("@chakra-ui/icons"); // src/react/components/OnboardingForm.tsx var import_react = require("react"); var import_react2 = require("@chakra-ui/react"); // src/react/assets/zerocat-logo.ts var import_meta = {}; 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 var import_jsx_runtime = require("react/jsx-runtime"); function OnboardingForm({ walletAddress, onComplete, onBack, theme = { primary: "blue", secondary: "teal" } }) { const toast = (0, import_react2.useToast)(); const [loading, setLoading] = (0, import_react.useState)(false); const [accountType, setAccountType] = (0, import_react.useState)("human"); const [errors, setErrors] = (0, import_react.useState)({}); const [availableRoles, setAvailableRoles] = (0, import_react.useState)(["User"]); const [defaultRole, setDefaultRole] = (0, import_react.useState)("User"); const [details, setDetails] = (0, import_react.useState)({ first_name: "", last_name: "", entity_name: "", email: "", role: "User" }); (0, import_react.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__ */ (0, import_jsx_runtime.jsxs)(import_react2.Box, { width: "100%", p: 6, position: "relative", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react2.VStack, { spacing: 6, align: "stretch", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react2.Box, { textAlign: "center", mb: 6, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.Flex, { justifyContent: "center", mb: 4, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_react2.Image, { src: zerocat_logo_default, alt: "ZEROCAT", height: "32px", width: "80px", objectFit: "contain" } ) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_react2.Heading, { as: "h1", size: "xl", bgGradient: `linear(to-r, ${theme.primary}.400, ${theme.secondary}.400)`, backgroundClip: "text", mb: 2, children: "Complete Your Profile" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.Text, { color: "whiteAlpha.700", children: "Tell us a bit about yourself" }) ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react2.VStack, { spacing: 6, align: "stretch", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react2.FormControl, { as: "fieldset", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.FormLabel, { as: "legend", fontWeight: "medium", children: "Account Type" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_react2.RadioGroup, { defaultValue: "human", onChange: (value) => setAccountType(value), value: accountType, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react2.Stack, { direction: "row", spacing: 5, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.Radio, { value: "human", children: "Individual" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.Radio, { value: "entity", children: "Entity/Organization" }) ] }) } ) ] }), accountType === "human" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react2.Stack, { direction: { base: "column", md: "row" }, spacing: 4, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react2.FormControl, { isInvalid: !!errors.first_name, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.FormLabel, { children: "First Name" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_react2.Input, { value: details.first_name, onChange: (e) => handleDetailsChange("first_name", e.target.value), placeholder: "John" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.FormErrorMessage, { children: errors.first_name }) ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react2.FormControl, { isInvalid: !!errors.last_name, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.FormLabel, { children: "Last Name" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_react2.Input, { value: details.last_name, onChange: (e) => handleDetailsChange("last_name", e.target.value), placeholder: "Doe" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.FormErrorMessage, { children: errors.last_name }) ] }) ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react2.FormControl, { isInvalid: !!errors.entity_name, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.FormLabel, { children: "Organization Name" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_react2.Input, { value: details.entity_name, onChange: (e) => handleDetailsChange("entity_name", e.target.value), placeholder: "Company or Organization Name" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.FormErrorMessage, { children: errors.entity_name }) ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react2.FormControl, { isInvalid: !!errors.email, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.FormLabel, { children: "Email" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_react2.Input, { type: "email", value: details.email, onChange: (e) => handleDetailsChange("email", e.target.value), placeholder: "you@example.com" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.FormErrorMessage, { children: errors.email }) ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react2.FormControl, { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.FormLabel, { children: "Role" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_react2.Select, { value: details.role, onChange: (e) => handleDetailsChange("role", e.target.value), children: availableRoles.map((role) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: role, children: role }, role)) } ) ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react2.Flex, { gap: 3, pt: 4, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_react2.Button, { flex: "1", variant: "outline", onClick: onBack, isDisabled: loading, children: "Back" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_react2.Button, { flex: "1", variant: "gradient", type: "submit", isLoading: loading, loadingText: "Creating...", children: "Complete" } ) ] }) ] }) }) ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( import_react2.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__ */ (0, import_jsx_runtime.jsx)(import_react2.Text, { fontSize: "xs", color: "whiteAlpha.600", mr: 1, children: "Powered by" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_react2.Image, { src: zerocat_logo_default, alt: "ZEROCAT", height: "12px", width: "28px" } ) ] } ) ] }); } // src/react/components/CapAuth.tsx var import_jsx_runtime2 = require("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: config2, onError }) { const toast = (0, import_react4.useToast)(); const { activeStep, setActiveStep } = (0, import_react4.useSteps)({ index: 0, count: steps.length }); const [stage, setStage] = (0, import_react3.useState)("intro"); const [qrData, setQrData] = (0, import_react3.useState)(null); const [qrUri, setQrUri] = (0, import_react3.useState)(null); const [error, setError] = (0, import_react3.useState)(null); const [nonce, setNonce] = (0, import_react3.useState)(null); const [walletAddress, setWalletAddress] = (0, import_react3.useState)(null); const [isPollingActive, setIsPollingActive] = (0, import_react3.useState)(true); const pollingIntervalRef = (0, import_react3.useRef)(null); const timeoutIdRef = (0, import_react3.useRef)(null); const isAuthenticatedRef = (0, import_react3.useRef)(false); const cleanupTimers = () => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } if (timeoutIdRef.current) { clearTimeout(timeoutIdRef.current); timeoutIdRef.current = null; } }; (0, import_react3.useEffect)(() => { return () => cleanupTimers(); }, []); (0, import_react3.useEffect)(() => { 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); } }, config2.timeouts?.polling || 3e3); pollingIntervalRef.current = interval; const timeout = setTimeout(() => { cleanupTimers(); setError("Authentication timeout. Please try again."); setStage("intro"); setActiveStep(0); }, config2.timeouts?.authentication || 3e5); timeoutIdRef.current = timeout; return () => cleanupTimers(); } }, [stage, nonce, isPollingActive, config2, 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 (config2.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" } } = config2; return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( import_react4.Box, { 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__ */ (0, import_jsx_runtime2.jsx)(import_react4.Box, { p: { base: 2, sm: 4 }, bg: "whiteAlpha.50", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Stepper, { index: activeStep, colorScheme: "brand", size: "sm", children: steps.map((step, index) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react4.Step, { children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.StepIndicator, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_react4.StepStatus, { complete: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.StepIcon, {}), incomplete: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.StepNumber, {}), active: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.StepNumber, {}) } ) }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Box, { flexShrink: 0, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.StepTitle, { fontSize: { base: "xs", sm: "sm" }, children: step.title }) }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.StepSeparator, {}) ] }, index)) }) }), /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_framer_motion.AnimatePresence, { mode: "wait", children: [ stage === "intro" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_framer_motion.motion.div, { variants: fadeIn, initial: "hidden", animate: "visible", exit: "exit", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react4.VStack, { spacing: { base: 4, sm: 6 }, p: { base: 4, sm: 6 }, children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react4.Box, { textAlign: "center", children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_react4.Heading, { as: "h1", size: { base: "lg", sm: "xl" }, bgGradient: `linear(to-r, ${theme.primary}.400, ${theme.secondary}.400)`, backgroundClip: "text", mb: 2, children: config2.appName } ), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Text, { color: "whiteAlpha.700", fontSize: { base: "sm", sm: "md" }, children: config2.appDescription }) ] }), /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react4.VStack, { spacing: 4, w: "full", children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( import_react4.Flex, { align: "flex-start", gap: 4, p: 4, bg: "whiteAlpha.50", borderRadius: "lg", w: "full", children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Box, { color: "blue.400", mt: 1, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_icons.PhoneIcon, { boxSize: { base: 4, sm: 5 } }) }), /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react4.Box, { children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Text, { fontWeight: "medium", fontSize: { base: "sm", sm: "md" }, children: "Get Your Wallet Ready" }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Text, { fontSize: { base: "xs", sm: "sm" }, color: "whiteAlpha.700", children: "Ensure you have ZeroWallet installed and ready" }) ] }) ] } ), /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( import_react4.Flex, { align: "flex-start", gap: 4, p: 4, bg: "whiteAlpha.50", borderRadius: "lg", w: "full", children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Box, { color: "teal.400", mt: 1, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_react4.Icon, { viewBox: "0 0 24 24", boxSize: { base: 4, sm: 5 }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "path", { fill: "currentColor", d: "M3 11h8V3H3v8zm2-6h4v4H5V5zm8-2v8h8V3h-8zm6 6h-4V5h4v4zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm13-2h-2v4h-4v2h4v4h2v-4h4v-2h-4v-4z" } ) } ) }), /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react4.Box, { children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Text, { fontWeight: "medium", fontSize: { base: "sm", sm: "md" }, children: "Credentialless Auth" }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Text, { fontSize: { base: "xs", sm: "sm" }, color: "whiteAlpha.700", children: "Scan QR code with your phone to open ZeroWallet" }) ] }) ] } ) ] }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_react4.Button, { w: "full", variant: "gradient", size: { base: "md", sm: "lg" }, onClick: generateQR, rightIcon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Icon, { viewBox: "0 0 24 24", boxSize: { base: 4, sm: 5 }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "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__ */ (0, import_jsx_runtime2.jsx)( import_framer_motion.motion.div, { variants: slideRight, initial: "hidden", animate: "visible", exit: "exit", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react4.VStack, { spacing: { base: 4, sm: 6 }, p: { base: 4, sm: 6 }, children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react4.Box, { textAlign: "center", children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Heading, { as: "h2", size: { base: "md", sm: "lg" }, mb: 2, children: "Scan QR Code" }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Text, { color: "whiteAlpha.700", fontSize: { base: "xs", sm: "sm" }, children: "Open your camera app and scan this code" }) ] }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Center, { py: { base: 4, sm: 6 }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Box, { p: { base: 3, sm: 4 }, bg: "white", borderRadius: "xl", boxShadow: "xl", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_qrcode.QRCodeSVG, { value: qrUri || "", size: Math.min(220, window.innerWidth * 0.6), level: "M", includeMargin: true } ) }) }), /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react4.VStack, { spacing: 4, w: "full", children: [ config2.enableMobileWallet && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_react4.Button, { w: "full", variant: "outline", onClick: handleOpenMobileWallet, leftIcon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_icons.PhoneIcon, {}), size: { base: "sm", sm: "md" }, children: "Open in ZeroWallet" } ), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_react4.Button, { w: "full", variant: "gradient", onClick: proceedToVerification, rightIcon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_icons.CheckIcon, {}), size: { base: "sm", sm: "md" }, children: "I've Completed the Transaction" } ) ] }) ] }) }, "qr" ), stage === "polling" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_framer_motion.motion.div, { variants: fadeIn, initial: "hidden", animate: "visible", exit: "exit", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react4.VStack, { spacing: 6, p: { base: 6, sm: 8 }, textAlign: "center", children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_react4.Spinner, { thickness: "4px", speed: "0.65s", emptyColor: "whiteAlpha.200", color: "brand.500", size: { base: "lg", sm: "xl" } } ), /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react4.Box, { children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Heading, { as: "h2", size: { base: "md", sm: "lg" }, mb: 2, children: "Verifying" }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Text, { color: "whiteAlpha.700", fontSize: { base: "sm", sm: "md" }, children: "Please wait while we confirm your transaction..." }) ] }) ] }) }, "polling" ), stage === "onboarding" && walletAddress && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_framer_motion.motion.div, { variants: slideRight, initial: "hidden", animate: "visible", exit: "exit", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( OnboardingForm, { walletAddress, onComplete: handleOnboardingComplete, onBack: handleRetry, theme } ) }, "onboarding" ) ] }), error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_framer_motion.motion.div, { initial: { opacity: 0, y: 10 }, animate: { opacity: 1, y: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Box, { p: { base: 3, sm: 4 }, bg: "red.900", color: "white", mt: 2, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react4.VStack, { spacing: { base: 3, sm: 4 }, children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Text, { color: "red.200", textAlign: "center", fontSize: { base: "sm", sm: "md" }, children: error }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_react4.Button, { leftIcon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_icons.RepeatIcon, {}), colorScheme: "red", variant: "outline", size: "sm", onClick: handleRetry, children: "Try Again" } ) ] }) }) } ), /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( import_react4.Flex, { justifyContent: "center", alignItems: "center", p: 2, borderTop: "1px", borderColor: "whiteAlpha.100", bg: "whiteAlpha.50", children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Text, { fontSize: "xs", color: "whiteAlpha.600", mr: 1, children: "Powered by" }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_react4.Image, { src: zerocat_logo_default, alt: "ZEROCAT", height: { base: "10px", sm: "12px" }, width: { base: "24px", sm: "28px" } } ) ] } ) ] } ); } // src/react/theme.ts var import_react5 = require("@chakra-ui/react"); var config = { initialColorMode: "dark", useSystemColorMode: false }; var colors = { brand: { 50: "#f0f9ff", 100: "#e0f2fe", 200: "#bae6fd", 300: "#7dd3fc", 400: "#38bdf8", 500: "#0ea5e9", 600: "#0284c7", 700: "#0369a1", 800: "#075985", 900: "#0c4a6e" }, accent: { 50: "#f0fdfa", 100: "#ccfbf1", 200: "#99f6e4", 300: "#5eead4", 400: "#2dd4bf", 500: "#14b8a6", 600: "#0d9488", 700: "#0f766e", 800: "#115e59", 900: "#134e4a" }, dark: { 100: "#1a202c", 200: "#171923", 300: "#0e1118", 400: "#0a0c11", 500: "#050609", gradient: "linear-gradient(180deg, #1a202c 0%, #0a0c11 100%)" } }; var components = { Button: { baseStyle: { fontWeight: "medium", borderRadius: "md" }, variants: { solid: { bg: "brand.500", color: "white", _hover: { bg: "brand.600", _disabled: { bg: "brand.500" } } }, outline: { borderColor: "whiteAlpha.300", color: "white", _hover: { bg: "whiteAlpha.100" } }, ghost: { color: "white", _hover: { bg: "whiteAlpha.100" } }, link: { color: "brand.500", _hover: { textDecoration: "none", color: "brand.600" } }, gradient: { bgGradient: "linear(to-r, brand.500, accent.500)", color: "white", _hover: { bgGradient: "linear(to-r, brand.600, accent.600)" } } } }, Modal: { baseStyle: { dialog: { bg: "dark.200", boxShadow: "xl", borderRadius: "xl", border: "1px solid", borderColor: "whiteAlpha.200" }, header: { borderBottom: "1px solid", borderColor: "whiteAlpha.100", pb: 4 }, body: { py: 6 }, footer: { borderTop: "1px solid", borderColor: "whiteAlpha.100", pt: 4 } } }, Card: { baseStyle: { container: { bg: "dark.200", boxShadow: "xl", borderRadius: "xl", border: "1px solid", borderColor: "whiteAlpha.200", overflow: "hidden" }, header: { py: 4, px: 6 }, body: { py: 4, px: 6 }, footer: { py: 4, px: 6 } } }, Input: { baseStyle: { field: { bg: "whiteAlpha.50", borderColor: "whiteAlpha.200", _hover: { borderColor: "whiteAlpha.300" }, _focus: { borderColor: "brand.500", boxShadow: "0 0 0 1px var(--chakra-colors-brand-500)" } } } }, Radio: { baseStyle: { control: { borderColor: "whiteAlpha.300", _checked: { bg: "brand.500", borderColor: "brand.500" } } } }, Tabs: { variants: { enclosed: { tab: { color: "whiteAlpha.700", _selected: { color: "white", bg: "whiteAlpha.100" } } }, "soft-rounded": { tab: { color: "whiteAlpha.700", _selected: { color: "white", bg: "brand.500" } } } } } }; var styles = { global: { "html, body": { color: "white", bg: "#0a0c11" } } }; var capAuthTheme = (0, import_react5.extendTheme)({ config, colors, components, styles }); var theme_default = capAuthTheme; // src/react/components/AuthProvider.tsx var import_jsx_runtime3 = require("react/jsx-runtime"); var AuthContext = (0, import_react6.createContext)(void 0); function AuthProvider({ children, config: config2 = {}, onAuthSuccess, onAuthError }) { const { isOpen, onOpen, onClose } = (0, import_react7.useDisclosure)(); const [user, setUser] = (0, import_react6.useState)(null); const defaultConfig = { appName: "Secure Authentication", appDescription: "Login securely with your wallet", theme: { primary: "blue", secondary: "teal" }, refreshPageOnAuth: false, timeouts: { authentication: 3e5, polling: 3e3 }, enableMobileWallet: true, mobileWalletScheme: "zerowallet://", ...config2 }; (0, import_react6.useEffect)(() => { const checkAuth = async () => { const walletAddress = localStorage.getItem("wallet_address"); if (walletAddress) { try { const response = await fetch("/api/users/active", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ wallet_address: walletAddress }) }); if (response.ok) { const data = await response.json(); setUser(data.user); } else { localStorage.removeItem("wallet_address"); } } catch (error) { console.error("Auth check failed:", error); } } }; checkAuth(); }, []); const handleAuthenticated = async (authenticatedUser) => { setUser(authenticatedUser); localStorage.setItem("wallet_address", authenticatedUser.wallet_address); onClose(); if (onAuthSuccess) { onAuthSuccess(authenticatedUser); } if (defaultConfig.refreshPageOnAuth) { window.location.reload(); } else if (defaultConfig.redirectPath) { window.location.href = defaultConfig.redirectPath; } }; const handleError = (error) => { console.error("Authentication error:", error); if (onAuthError) { onAuthError(error); } }; const showAuthModal = () => onOpen(); const hideAuthModal = () => onClose(); const logout = () => { localStorage.removeItem("wallet_address"); setUser(null); }; const contextValue = { user, isAuthenticated: !!user, showAuthModal, hideAuthModal, logout }; return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AuthContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react7.ChakraProvider, { theme: theme_default, children: [ children, /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)( import_react7.Modal, { isOpen, onClose, isCentered: true, size: "md", closeOnOverlayClick: false, children: [ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react7.ModalOverlay, { backdropFilter: "blur(10px)", bg: "blackAlpha.700" }), /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)( import_react7.ModalContent, { bg: "transparent", boxShadow: "none", width: "auto", maxWidth: "100%", margin: "0", children: [ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( import_react7.ModalCloseButton, { color: "white", zIndex: "popover", position: "absolute", right: "4px", top: "4px" } ), /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react7.ModalBody, { p: 0, display: "flex", justifyContent: "center", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( CapAuth, { onAuthenticated: handleAuthenticated, config: defaultConfig, onError: handleError } ) }) ] } ) ] } ) ] }) }); } function useAuth() { const context = (0, import_react6.useContext)(AuthContext); if (context === void 0) { throw new Error("useAuth must be used within an AuthProvider"); } return context; } function withAuth(WrappedComponent) { return function WithAuthComponent(props) { const { isAuthenticated, showAuthModal } = useAuth(); (0, import_react6.useEffect)(() => { if (!isAuthenticated) { showAuthModal(); } }, [isAuthenticated]); return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(WrappedComponent, { ...props }); }; } // src/server/db/encrypted-json-db.ts var import_fs = require("fs"); var import_path = require("path"); var import_crypto = require("crypto"); var defaultSchema = { users: [] }; var EncryptedJSONDatabase = class _EncryptedJSONDatabase { static instance; dbPath; data = defaultSchema; initialized = false; encryptionKey; constructor(dbPath) { this.dbPath = dbPath || (0, import_path.join)(process.cwd(), "data", "caplib-users.encrypted.json"); const key = process.env.DB_ENCRYPTION_KEY || (0, import_crypto.randomBytes)(32).toString("hex"); this.encryptionKey = Buffer.from(key, "hex"); } static getInstance(dbPath) { if (!_EncryptedJSONDatabase.instance) { _EncryptedJSONDatabase.instance = new _EncryptedJSONDatabase(dbPath); } return _EncryptedJSONDatabase.instance; } async encrypt(data) { const iv = (0, import_crypto.randomBytes)(16); const cipher = (0, import_crypto.createCipheriv)("aes-256-gcm", this.encryptionKey, iv); let encrypted = cipher.update(data, "utf8", "hex"); encrypted += cipher.final("hex"); const authTag = cipher.getAuthTag(); return JSON.stringify({ iv: iv.toString("hex"), data: encrypted, authTag: authTag.toString("hex") }); } async decrypt(encryptedData) { const { iv, data, authTag } = JSON.parse(encryptedData); const decipher = (0, import_crypto.createDecipheriv)( "aes-256-gcm", this.encryptionKey, Buffer.from(iv, "hex") ); decipher.setAuthTag(Buffer.from(authTag, "hex")); let decrypted = decipher.update(data, "hex", "utf8"); decrypted += decipher.final("utf8"); return decrypted; } async ensureDirectoryExists() { const dir = this.dbPath.substring(0, this.dbPath.lastIndexOf("/")); try { await import_fs.promises.access(dir); } catch { await import_fs.promises.mkdir(dir, { recursive: true }); } } async loadData() { try { const encryptedContent = await import_fs.promises.readFile(this.dbPath, "utf-8"); const decryptedContent = await this.decrypt(encryptedContent); this.data = JSON.parse(decryptedContent); } catch (error) { if (error.code === "ENOENT") { this.data = defaultSchema; await this.saveData(); } else { throw error; } } } async saveData() { await this.ensureDirectoryExists(); const encryptedData = await this.encrypt(JSON.stringify(this.data)); await import_fs.promises.writeFile(this.dbPath, encryptedData); } async initialize() { if (this.initialized) return; try { await this.loadData(); this.initialized = true; } catch (error) { console.error("Failed to initialize database:", error); throw error; } } async findUser(walletAddress) { await this.initialize(); const user = this.data.users.find( (u) => u.wallet_address.toLowerCase() === walletAddress.toLowerCase() ); return user || null; } async createUser(userData) { await this.initialize(); const existingUser = await this.findUser(userData.wallet_address); if (existingUser) { throw new Error("User already exists"); } const now = (/* @__PURE__ */ new Date()).toISOString(); const newUser = { ...userData, role: userData.role || "User", account_type: userData.account_type || "human", created_at: now }; this.data.users.push(newUser); await this.saveData(); return newUser; } async updateUser(walletAddress, updates) { await this.initialize(); const index = this.data.users.findIndex( (u) => u.wallet_address.toLowerCase() === walletAddress.toLowerCase() ); if (index === -1) return null; const updatedUser = { ...this.data.users[index], ...updates, wallet_address: this.data.users[index].wallet_address }; this.data.users[index] = updatedUser; await this.saveData(); return updatedUser; } async deleteUser(walletAddress) { await this.initialize(); const initialLength = this.data.users.length; this.data.users = this.data.users.filter( (u) => u.wallet_address.toLowerCase() !== walletAddress.toLowerCase() ); if (this.data.users.length !== initialLength) { await this.saveData(); return true; } return false; } async close() { await this.saveData(); this.initialized = false; } }; // src/server/utils/roles.ts function getAvailableRoles() { const roles = process.env.USER_ROLES || "User,Administrator"; return roles.split(",").map((role) => role.trim()).filter(Boolean); } function getDefaultRole() { return process.env.DEFAULT_USER_ROLE || "User"; } function isValidRole(role) { const availableRoles = getAvailableRoles(); return availableRoles.includes(role); } // src/server/handlers/wallet-auth.ts var getEnvVariable = (name, fallbackName) => { let value = process.env[name]; if (!value && fallbackName) { value = process.env[fallbackName]; } if (!value) { throw new Error(`Required environment variable ${name}${fallbackName ? ` or ${fallbackName}` : ""} is not set`); } return value; }; var createAuthHandler = (config2) => { const db = EncryptedJSONDatabase.getInstance(); let isInitialized = false; const initialize = async () => { if (!isInitialized) { try { await db.initialize(); isInitialized = true; } catch (error) { console.error("Failed to initialize database:", error); } } }; const GET = async () => { try { const API_URL = getEnvVariable("CAPLIB_API_URL"); const API_KEY = getEnvVariable("CAPLIB_API_KEY"); const CONTRACT_ID = getEnvVariable("NEXT_PUBLIC_AUTH_CONTRACT_ID", "AUTH_CONTRACT_ID"); await initialize(); const response = await fetch(`${API_URL}?action=generateNonce&contractId=${CONTRACT_ID}`, { headers: { "X-API-Key": API_KEY } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); return new Response( JSON.stringify({ nonce: data.nonce }), { status: 200, headers: { "Content-Type": "application/json" } } ); } catch (error) { console.error("Error generating nonce:", error); return new Response( JSON.stringify({ error: "Failed to generate nonce", nonce: "" }), { status: 500, headers: { "Content-Type": "application/json" } } ); } }; const POST = async (req) => { try { const API_URL = getEnvVariable("CAPLIB_API_URL"); const API_KEY = getEnvVariable("CAPLIB_API_KEY"); const CONTRACT_ID = getEnvVariable("NEXT_PUBLIC_AUTH_CONTRACT_ID", "AUTH_CONTRACT_ID"); await initialize(); const body = await req.json(); const { nonce, userData } = body; const verifyResponse = await fetch(API_URL, { method: "POST", headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ action: "verifyAuthentication", contractId: CONTRACT_ID, nonce }) }); if (!verifyResponse.ok) { throw new Error(`Verification failed: ${verifyResponse.status}`); } const verifyData = await verifyResponse.json(); const { authenticated, userAddress } = verifyData; if (!authenticated || !userAddress) { return new Response( JSON.stringify({ authenticated: false, error: "Authentication failed" }), { status: 401, headers: { "Content-Type": "application/json" } } ); } if (config2?.customValidation) { const isValid = await config2.customValidation(userAddress); if (!isValid) { return new Response( JSON.stringify({ authenticated: false, error: "Custom validation failed" }), { status: 403, headers: { "Content-Type": "application/json" } } ); } } try { let user = await db.findUser(userAddress); if (userData?.role && !isValidRole(userData.role)) { userData.role = getDefaultRole(); } if (!user && userData) { user = await db.createUser({ wallet_address: userAddress,