bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
316 lines (315 loc) • 11.1 kB
JavaScript
// 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;
}