caplib
Version:
Credentialless Authentication Protocol Library for Web Applications
312 lines (309 loc) • 9.28 kB
JavaScript
import {
getDefaultRole,
isValidRole
} from "./chunk-7AEYXUST.mjs";
// src/server/db/encrypted-json-db.ts
import { promises as fs } from "fs";
import { join } from "path";
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
var defaultSchema = {
users: []
};
var EncryptedJSONDatabase = class _EncryptedJSONDatabase {
static instance;
dbPath;
data = defaultSchema;
initialized = false;
encryptionKey;
constructor(dbPath) {
this.dbPath = dbPath || join(process.cwd(), "data", "caplib-users.encrypted.json");
const key = process.env.DB_ENCRYPTION_KEY || 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 = randomBytes(16);
const cipher = 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 = 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 fs.access(dir);
} catch {
await fs.mkdir(dir, { recursive: true });
}
}
async loadData() {
try {
const encryptedContent = await fs.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 fs.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/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 = (config) => {
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 (config?.customValidation) {
const isValid = await config.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,
...userData
});
}
return new Response(
JSON.stringify({
authenticated: true,
userAddress,
user: user || void 0
}),
{
status: 200,
headers: { "Content-Type": "application/json" }
}
);
} catch (error) {
console.error("Database error:", error);
return new Response(
JSON.stringify({
authenticated: true,
userAddress,
error: "Database operation failed"
}),
{
status: 200,
headers: { "Content-Type": "application/json" }
}
);
}
} catch (error) {
console.error("Error verifying authentication:", error);
return new Response(
JSON.stringify({
authenticated: false,
error: "Authentication verification failed"
}),
{
status: 500,
headers: { "Content-Type": "application/json" }
}
);
}
};
return { GET, POST };
};
export {
EncryptedJSONDatabase,
createAuthHandler
};
//# sourceMappingURL=chunk-RW7N3RE6.mjs.map