UNPKG

bigblocks

Version:

Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React

316 lines (315 loc) 11.1 kB
// BigBlocks Core - Framework-agnostic Bitcoin authentication // Based on Better Auth patterns using Web API standards /// <reference lib="dom" /> import { HD, Mnemonic } from "@bsv/sdk"; import { decryptBackup, encryptBackup, } from "bitcoin-backup"; import { BAP } from "bsv-bap"; import { DEFAULT_ALLOWED_BACKUP_TYPES } from "../lib/backup-utils.js"; import { createMemoryStorage } from "./storage/index.js"; // Create default storage adapter based on environment function createDefaultStorage() { // Use memory storage as default (works everywhere) return createMemoryStorage(); } /** * Creates a BigBlocks authentication instance * Following Better Auth pattern with universal handler */ export function createBigBlocksAuth(options = {}) { const config = { verifySignatures: options.verifySignatures ?? false, timeWindow: options.timeWindow ?? 300, bodyEncoding: options.bodyEncoding ?? "utf8", basePath: options.basePath ?? "/api", storage: options.storage ?? createDefaultStorage(), storageNamespace: options.storageNamespace ?? "bigblocks", backupTypes: options.backupTypes ?? { enabled: DEFAULT_ALLOWED_BACKUP_TYPES, }, }; // Universal handler that works with any framework const handler = async (request) => { try { const url = new URL(request.url); const pathname = url.pathname; // Extract endpoint from path const endpoint = pathname.replace(config.basePath, "").replace(/^\//, ""); switch (endpoint) { case "auth/signin": return await handleSignIn(request, config); case "auth/signup": return await handleSignUp(request, config); case "auth/signout": return await handleSignOut(request, config); case "auth/session": return await handleGetSession(request, config); default: return new globalThis.Response(JSON.stringify({ error: "Endpoint not found" }), { status: 404, headers: { "Content-Type": "application/json" }, }); } } catch (error) { console.error("BigBlocks auth error:", error); return new globalThis.Response(JSON.stringify({ error: "Internal server error" }), { status: 500, headers: { "Content-Type": "application/json" }, }); } }; // API for direct usage (not through HTTP) const api = { verifyAuth: async (payload) => { return verifyBitcoinAuth(payload, config); }, getSession: async (headers) => { return getSessionFromHeaders(headers, config); }, generateBackup: () => { return generateBackup(); }, encryptBackup: async (backup, password) => { return await encryptBackup(backup, password); }, decryptBackup: async (encryptedBackup, password) => { return await decryptBackup(encryptedBackup, password); }, }; return { handler, api, options: config, }; } // Handler implementations async function handleSignIn(request, config) { try { const authToken = request.headers.get("X-Auth-Token"); const body = await request.json(); const { address, idKey } = body; if (!authToken || !address || !idKey) { return new globalThis.Response(JSON.stringify({ error: "Missing required fields" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } // Verify Bitcoin signature if enabled if (config.verifySignatures) { const payload = { address, idKey, authToken, requestPath: new URL(request.url).pathname, body: JSON.stringify({ address, idKey }), }; const authResult = await verifyBitcoinAuth(payload, config); if (!authResult.success) { return new globalThis.Response(JSON.stringify({ error: authResult.error }), { status: 401, headers: { "Content-Type": "application/json" }, }); } } // For development: Skip signature verification and return mock user const user = { id: idKey, address, idKey, profiles: [ { id: idKey, address, isPublished: false, }, ], activeProfileId: idKey, }; return new globalThis.Response(JSON.stringify({ success: true, user, }), { headers: { "Content-Type": "application/json" }, }); } catch (error) { console.error("SignIn error:", error); return new globalThis.Response(JSON.stringify({ error: "Authentication failed" }), { status: 500, headers: { "Content-Type": "application/json" }, }); } } async function handleSignUp(request, _config) { try { const body = await request.json(); const { password } = body; if (!password) { return new globalThis.Response(JSON.stringify({ error: "Password required" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } // Generate new backup const backup = generateBackup(); // Encrypt backup const encryptedBackup = await encryptBackup(backup, password); // In a real implementation, you'd store this in your database // For now, return the backup for client-side storage return new globalThis.Response(JSON.stringify({ success: true, backup: encryptedBackup, user: { id: "mock-id", address: "mock-address", idKey: "mock-id", profiles: [ { id: "mock-id", address: "mock-address", isPublished: false, }, ], activeProfileId: "mock-id", }, }), { headers: { "Content-Type": "application/json" }, }); } catch (error) { console.error("SignUp error:", error); return new globalThis.Response(JSON.stringify({ error: "Account creation failed" }), { status: 500, headers: { "Content-Type": "application/json" }, }); } } async function handleSignOut(request, config) { // Invalidate session in storage try { const authToken = request.headers.get("X-Auth-Token"); if (authToken && config.storage) { // Remove session from storage const sessionKey = `${config.storageNamespace}:session:${authToken}`; await config.storage.delete(sessionKey); } } catch (error) { console.error("Error invalidating session:", error); } return new globalThis.Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" }, }); } async function handleGetSession(request, config) { const session = await getSessionFromHeaders(request.headers, config); if (!session) { return new globalThis.Response(JSON.stringify({ error: "No session found" }), { status: 401, headers: { "Content-Type": "application/json" }, }); } return new globalThis.Response(JSON.stringify({ session }), { headers: { "Content-Type": "application/json" }, }); } // Core verification logic async function verifyBitcoinAuth(payload, config) { try { if (!config.verifySignatures) { // Development mode - skip verification const user = { id: payload.idKey, address: payload.address, idKey: payload.idKey, profiles: [ { id: payload.idKey, address: payload.address, isPublished: false, }, ], activeProfileId: payload.idKey, }; return { success: true, user }; } // Import BSM verification from bitcoin-auth const { verifyAuthToken } = await import("bitcoin-auth"); const target = { requestPath: payload.requestPath, body: payload.body, timestamp: payload.timestamp || "", }; const isValid = verifyAuthToken(payload.authToken, target); if (!isValid) { return { success: false, error: "Invalid Bitcoin signature", }; } const user = { id: payload.idKey, address: payload.address, idKey: payload.idKey, profiles: [ { id: payload.idKey, address: payload.address, isPublished: false, }, ], activeProfileId: payload.idKey, }; return { success: true, user }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Verification failed", }; } } async function getSessionFromHeaders(headers, config) { try { const authToken = headers.get("X-Auth-Token"); if (!authToken || !config.storage) { return null; } // Check if session exists in storage const sessionKey = `${config.storageNamespace}:session:${authToken}`; const sessionData = await config.storage.get(sessionKey); if (!sessionData) { return null; } const session = JSON.parse(sessionData); // Check if session is expired if (session.expiresAt < new Date()) { // Remove expired session await config.storage.delete(sessionKey); return null; } return session; } catch (error) { console.error("Error retrieving session:", error); return null; } } // Backup generation (extracted from AuthManager) function generateBackup() { // Generate a new identity const mnemonic = Mnemonic.fromRandom(); const seed = mnemonic.toSeed(); const hdKey = HD.fromSeed(seed); const xprv = hdKey.toString(); const bap = new BAP(xprv); bap.newId(); // Create root identity const ids = bap.exportIds(); const backup = { xprv, mnemonic: mnemonic.toString(), ids, createdAt: new Date().toISOString(), }; return backup; }