caplib
Version:
Credentialless Authentication Protocol Library for Web Applications
1,392 lines (1,379 loc) • 51.3 kB
JavaScript
"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,