UNPKG

electron-license-kit

Version:

Secure licensing, storage, and utilities for Electron apps with Supabase

1,271 lines (1,239 loc) 36.1 kB
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // src/branding/define-config.ts var defaultTheme = { primary: "#6366f1", primaryHover: "#4f46e5", primaryText: "#ffffff", background: "#0a0a0f", backgroundSecondary: "#111118", backgroundTertiary: "#1a1a24", text: "#ffffff", textSecondary: "#a1a1aa", textTertiary: "#71717a", success: "#22c55e", warning: "#f59e0b", error: "#ef4444", info: "#3b82f6", border: "#27272a", divider: "#3f3f46", titlebar: { background: "#0a0a0f", text: "#ffffff", buttonHover: "#27272a", buttonClose: "#ef4444" } }; var defaultConfig = { app: { name: "My App", id: "com.example.myapp", version: "1.0.0" }, branding: {}, theme: defaultTheme, license: { keyPrefix: "APP", keyPattern: /^APP-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/, offlineGraceDays: 3, checkIntervalHours: 24 }, supabase: { tableName: "licenses", claimFunction: "claim_license" }, window: { width: 800, height: 600, minWidth: 700, minHeight: 500, frame: false, resizable: true }, features: { autoUpdater: true, crashReporter: true, singleInstance: true, devTools: false } }; function defineConfig(config) { return { app: { ...defaultConfig.app, ...config.app }, branding: { ...defaultConfig.branding, ...config.branding }, theme: { ...defaultConfig.theme, ...config.theme, titlebar: { ...defaultConfig.theme.titlebar, ...config.theme?.titlebar } }, license: { ...defaultConfig.license, ...config.license }, supabase: { ...defaultConfig.supabase, ...config.supabase }, window: { ...defaultConfig.window, ...config.window }, features: { ...defaultConfig.features, ...config.features } }; } // src/branding/presets.ts var darkPreset = { primary: "#6366f1", primaryHover: "#4f46e5", primaryText: "#ffffff", background: "#0a0a0f", backgroundSecondary: "#111118", backgroundTertiary: "#1a1a24", text: "#ffffff", textSecondary: "#a1a1aa", textTertiary: "#71717a", border: "#27272a", divider: "#3f3f46" }; var lightPreset = { primary: "#4f46e5", primaryHover: "#4338ca", primaryText: "#ffffff", background: "#ffffff", backgroundSecondary: "#f4f4f5", backgroundTertiary: "#e4e4e7", text: "#18181b", textSecondary: "#52525b", textTertiary: "#a1a1aa", border: "#e4e4e7", divider: "#d4d4d8" }; var midnightPreset = { primary: "#3b82f6", primaryHover: "#2563eb", primaryText: "#ffffff", background: "#0f172a", backgroundSecondary: "#1e293b", backgroundTertiary: "#334155", text: "#f1f5f9", textSecondary: "#94a3b8", textTertiary: "#64748b", border: "#334155", divider: "#475569" }; var forestPreset = { primary: "#22c55e", primaryHover: "#16a34a", primaryText: "#ffffff", background: "#0a0f0a", backgroundSecondary: "#111811", backgroundTertiary: "#1a241a", text: "#f0fdf4", textSecondary: "#86efac", textTertiary: "#4ade80", border: "#27372a", divider: "#3f4f46" }; var presets = { dark: darkPreset, light: lightPreset, midnight: midnightPreset, forest: forestPreset }; // src/branding/css-generator.ts function hexToRgb(hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); if (!result) return "0, 0, 0"; const r = result[1] ?? "00"; const g = result[2] ?? "00"; const b = result[3] ?? "00"; return `${parseInt(r, 16)}, ${parseInt(g, 16)}, ${parseInt(b, 16)}`; } function generateCSSVariables(theme) { return `:root { /* Primary */ --elk-primary: ${theme.primary}; --elk-primary-hover: ${theme.primaryHover}; --elk-primary-text: ${theme.primaryText}; --elk-primary-rgb: ${hexToRgb(theme.primary)}; /* Backgrounds */ --elk-bg: ${theme.background}; --elk-bg-secondary: ${theme.backgroundSecondary}; --elk-bg-tertiary: ${theme.backgroundTertiary}; /* Text */ --elk-text: ${theme.text}; --elk-text-secondary: ${theme.textSecondary}; --elk-text-tertiary: ${theme.textTertiary}; /* Semantic */ --elk-success: ${theme.success}; --elk-warning: ${theme.warning}; --elk-error: ${theme.error}; --elk-info: ${theme.info}; /* Border */ --elk-border: ${theme.border}; --elk-divider: ${theme.divider}; /* Titlebar */ --elk-titlebar-bg: ${theme.titlebar.background}; --elk-titlebar-text: ${theme.titlebar.text}; --elk-titlebar-btn-hover: ${theme.titlebar.buttonHover}; --elk-titlebar-btn-close: ${theme.titlebar.buttonClose}; /* Computed */ --elk-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5); --elk-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.5); --elk-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5); --elk-radius-sm: 4px; --elk-radius-md: 8px; --elk-radius-lg: 12px; --elk-radius-full: 9999px; }`; } function generateStyleTag(theme) { return `<style id="elk-theme"> ${generateCSSVariables(theme)} </style>`; } // src/branding/theme-injector.ts async function injectTheme(window, theme) { const css = generateCSSVariables(theme); await window.webContents.executeJavaScript(` (function() { const existing = document.getElementById('elk-theme'); if (existing) existing.remove(); const style = document.createElement('style'); style.id = 'elk-theme'; style.textContent = ${JSON.stringify(css)}; document.head.appendChild(style); })(); `); } function injectThemeOnReady(window, theme) { window.webContents.on("dom-ready", () => { void injectTheme(window, theme); }); } // src/license/hwid-generator.ts import * as crypto from "crypto"; import * as os from "os"; import { machineIdSync } from "node-machine-id"; function getMachineId() { try { return machineIdSync(true); } catch { return os.hostname() + os.userInfo().username; } } function getCPUModel() { const cpus2 = os.cpus(); const first = cpus2[0]; return first?.model ?? "unknown"; } function getHWIDComponents() { return { machineId: getMachineId(), platform: os.platform(), arch: os.arch(), cpuModel: getCPUModel() }; } function generateHWID() { const components = getHWIDComponents(); const combined = [ components.machineId, components.platform, components.arch, components.cpuModel ].join("|"); const hash = crypto.createHash("sha256"); hash.update(combined); return hash.digest("hex"); } // src/storage/secure-storage.ts import * as crypto2 from "crypto"; import * as fs from "fs"; import * as path from "path"; import * as os2 from "os"; var ALGORITHM = "aes-256-gcm"; var SecureStorage = class { storageDir; filePath; encryptionKey; constructor(config) { const appName = config.appName ?? "ElectronApp"; this.storageDir = config.storageDir ?? path.join(os2.homedir(), "AppData", "Roaming", appName); this.filePath = path.join(this.storageDir, config.fileName); this.encryptionKey = config.encryptionKey ?? this.deriveKey(); } deriveKey() { const hwid = generateHWID(); return crypto2.createHash("sha256").update(hwid).digest(); } encrypt(data) { const iv = crypto2.randomBytes(16); const cipher = crypto2.createCipheriv(ALGORITHM, this.encryptionKey, iv); let encrypted = cipher.update(JSON.stringify(data), "utf8", "hex"); encrypted += cipher.final("hex"); const authTag = cipher.getAuthTag(); return { iv: iv.toString("hex"), data: encrypted, tag: authTag.toString("hex") }; } decrypt(encrypted) { try { const iv = Buffer.from(encrypted.iv, "hex"); const authTag = Buffer.from(encrypted.tag, "hex"); const decipher = crypto2.createDecipheriv(ALGORITHM, this.encryptionKey, iv); decipher.setAuthTag(authTag); let decrypted = decipher.update(encrypted.data, "hex", "utf8"); decrypted += decipher.final("utf8"); return JSON.parse(decrypted); } catch (error) { console.error("Decryption failed:", error); return null; } } save(data) { try { fs.mkdirSync(this.storageDir, { recursive: true }); const encrypted = this.encrypt(data); fs.writeFileSync(this.filePath, JSON.stringify(encrypted), "utf8"); return true; } catch (error) { console.error("Failed to save:", error); return false; } } load() { try { if (!fs.existsSync(this.filePath)) { return null; } const encrypted = JSON.parse(fs.readFileSync(this.filePath, "utf8")); return this.decrypt(encrypted); } catch (error) { console.error("Failed to load:", error); return null; } } exists() { try { return fs.existsSync(this.filePath) && this.load() !== null; } catch { return false; } } clear() { try { if (fs.existsSync(this.filePath)) { fs.unlinkSync(this.filePath); } return true; } catch (error) { console.error("Failed to clear:", error); return false; } } getPath() { return this.filePath; } }; // src/license/license-cache.ts var DEFAULT_MAX_AGE_DAYS = 3; var LicenseCache = class { storage; maxAgeDays; constructor(config) { this.storage = new SecureStorage({ fileName: config?.cacheFileName ?? "license.dat", appName: config?.appName, storageDir: config?.cacheDir }); this.maxAgeDays = config?.maxAgeDays ?? DEFAULT_MAX_AGE_DAYS; } save(data) { return this.storage.save(data); } load() { return this.storage.load(); } isValid() { const data = this.load(); if (!data) return false; if (data.lastCheck) { const lastCheck = new Date(data.lastCheck); const now = /* @__PURE__ */ new Date(); const daysSinceCheck = (now.getTime() - lastCheck.getTime()) / (1e3 * 60 * 60 * 24); if (daysSinceCheck > this.maxAgeDays) { return false; } } if (data.tier !== "lifetime" && data.expiresAt) { const expires = new Date(data.expiresAt); if (expires < /* @__PURE__ */ new Date()) { return false; } } return true; } clear() { return this.storage.clear(); } getDaysRemaining() { const data = this.load(); if (!data?.expiresAt) return null; if (data.tier === "lifetime") return Infinity; const expires = new Date(data.expiresAt); const now = /* @__PURE__ */ new Date(); const days = Math.ceil((expires.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24)); return Math.max(0, days); } }; // src/license/license-service.ts import { createClient } from "@supabase/supabase-js"; // src/storage/key-manager.ts var KeyManager = class { storage; constructor(appName) { this.storage = new SecureStorage({ fileName: "key.dat", appName }); } save(licenseKey) { if (!licenseKey || typeof licenseKey !== "string") { console.error("Invalid license key"); return false; } return this.storage.save({ licenseKey, savedAt: (/* @__PURE__ */ new Date()).toISOString() }); } load() { const data = this.storage.load(); return data?.licenseKey ?? null; } exists() { return this.storage.exists(); } clear() { return this.storage.clear(); } getKeyPath() { return this.storage.getPath(); } }; // src/license/license-service.ts var DEFAULT_KEY_PATTERN = /^[A-Z]{2}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/; var LicenseService = class { supabase; cache; keyManager; keyPattern; tableName; claimRpcName; currentUser = null; currentLicense = null; constructor(config) { this.validateKey(config.supabaseAnonKey); this.supabase = createClient(config.supabaseUrl, config.supabaseAnonKey); this.cache = new LicenseCache({ appName: config.appName }); this.keyManager = new KeyManager(config.appName); this.keyPattern = config.licenseKeyPattern ?? DEFAULT_KEY_PATTERN; this.tableName = config.tableName ?? "licenses"; this.claimRpcName = config.claimRpcName ?? "claim_license"; } validateKey(key) { try { const parts = key.split("."); if (parts.length < 2) { return; } const payloadSegment = parts[1]; if (!payloadSegment) { return; } const payloadJson = Buffer.from(payloadSegment, "base64").toString(); const payload = JSON.parse(payloadJson); if (payload.role === "service_role") { throw new Error("SECURITY: Service role key detected. Use anon key only!"); } } catch (e) { if (e instanceof Error && e.message.includes("SECURITY")) throw e; } } async register(email, password, licenseKey) { if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { throw new Error("Invalid email address"); } if (!password || password.length < 8) { throw new Error("Password must be at least 8 characters"); } if (!licenseKey || !this.keyPattern.test(licenseKey.toUpperCase())) { throw new Error("Invalid license key format"); } const normalizedKey = licenseKey.toUpperCase(); const { data: license, error: licenseError } = await this.supabase.from(this.tableName).select("*").eq("license_key", normalizedKey).single(); if (licenseError) { throw new Error(licenseError.message); } if (!license) { throw new Error("License key not found"); } if (license.user_id) { throw new Error("License already activated"); } if (license.status !== "active") { throw new Error(`License is ${license.status}`); } const { data: authData, error: authError } = await this.supabase.auth.signUp({ email, password }); if (authError) { throw new Error(authError.message); } if (!authData.user) { throw new Error("Registration failed"); } const hwid = generateHWID(); const { data: claimResult, error: claimError } = await this.supabase.rpc(this.claimRpcName, { p_license_key: normalizedKey, p_user_id: authData.user.id, p_hwid: hwid }); if (claimError) { throw new Error(claimError.message); } if (!claimResult?.success) { throw new Error(claimResult?.error ?? "License activation failed"); } this.cache.save({ email, tier: license.tier, expiresAt: license.expires_at, lastCheck: (/* @__PURE__ */ new Date()).toISOString(), hwid }); this.keyManager.save(normalizedKey); this.currentUser = { id: authData.user.id, email }; this.currentLicense = { ...license, hwid, user_id: authData.user.id }; return { user: this.currentUser, license: this.currentLicense }; } async login(email, password) { if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { throw new Error("Invalid email address"); } if (!password) { throw new Error("Password is required"); } const { data: authData, error: authError } = await this.supabase.auth.signInWithPassword({ email, password }); if (authError) { throw new Error(authError.message); } if (!authData.user) { throw new Error("Login failed"); } const { data: license, error: licenseError } = await this.supabase.from(this.tableName).select("*").eq("user_id", authData.user.id).eq("status", "active").single(); if (licenseError) { throw new Error(licenseError.message); } if (!license) { throw new Error("No active license found"); } const hwid = generateHWID(); if (license.hwid && license.hwid !== hwid) { throw new Error("License bound to different computer"); } await this.supabase.from(this.tableName).update({ last_check: (/* @__PURE__ */ new Date()).toISOString(), hwid: license.hwid ?? hwid }).eq("id", license.id); this.cache.save({ email, tier: license.tier, expiresAt: license.expires_at, lastCheck: (/* @__PURE__ */ new Date()).toISOString(), hwid }); if (license.license_key) { this.keyManager.save(license.license_key); } this.currentUser = { id: authData.user.id, email }; this.currentLicense = license; return { user: this.currentUser, license }; } async validate() { try { const { data: sessionData } = await this.supabase.auth.getSession(); const session = sessionData.session; if (session) { const { data: license } = await this.supabase.from(this.tableName).select("*").eq("user_id", session.user.id).eq("status", "active").single(); if (license) { const hwid = generateHWID(); if (license.hwid && license.hwid !== hwid) { return { valid: false, error: "HWID mismatch" }; } this.cache.save({ email: session.user.email ?? "", tier: license.tier, expiresAt: license.expires_at, lastCheck: (/* @__PURE__ */ new Date()).toISOString(), hwid }); this.currentUser = { id: session.user.id, email: session.user.email ?? "" }; this.currentLicense = license; return { valid: true, license: this.cache.load(), user: this.currentUser, source: "online" }; } } } catch { } if (this.cache.isValid()) { return { valid: true, license: this.cache.load(), source: "cache" }; } return { valid: false, error: "No valid license found" }; } async logout() { await this.supabase.auth.signOut(); this.cache.clear(); this.keyManager.clear(); this.currentUser = null; this.currentLicense = null; } getStatus() { const cached = this.cache.load(); if (!cached) return null; return { email: cached.email, tier: cached.tier, daysRemaining: this.cache.getDaysRemaining(), isLifetime: cached.tier === "lifetime" }; } async resetPassword(email) { if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { throw new Error("Invalid email address"); } const { error } = await this.supabase.auth.resetPasswordForEmail(email); if (error) throw new Error(error.message); return true; } }; // src/window/window-manager.ts import { BrowserWindow, app } from "electron"; function createMainWindow(config) { const window = new BrowserWindow({ width: config.width ?? 800, height: config.height ?? 600, minWidth: config.minWidth ?? 700, minHeight: config.minHeight ?? 500, frame: config.frame ?? false, resizable: true, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: config.preloadPath, sandbox: true, webSecurity: true }, backgroundColor: config.backgroundColor ?? "#0a0a0f", show: false }); void window.loadFile(config.htmlPath); window.once("ready-to-show", () => { window.show(); }); const shouldOpenDevTools = config.devTools ?? process.argv.includes("--dev"); if (shouldOpenDevTools && !app.isPackaged) { window.webContents.openDevTools(); } return window; } // src/window/window-controls.ts import { ipcMain } from "electron"; function registerWindowControls(window) { ipcMain.on("window-minimize", () => { if (window && !window.isDestroyed()) { window.minimize(); } }); ipcMain.on("window-maximize", () => { if (window && !window.isDestroyed()) { if (window.isMaximized()) { window.unmaximize(); } else { window.maximize(); } } }); ipcMain.on("window-close", () => { if (window && !window.isDestroyed()) { window.close(); } }); } function unregisterWindowControls() { ipcMain.removeAllListeners("window-minimize"); ipcMain.removeAllListeners("window-maximize"); ipcMain.removeAllListeners("window-close"); } // src/window/single-instance.ts import { app as app2, BrowserWindow as BrowserWindow3 } from "electron"; function enforceSingleInstance(config) { const gotLock = app2.requestSingleInstanceLock(); if (!gotLock) { return false; } app2.on("second-instance", (_event, argv) => { if (config?.onSecondInstance) { config.onSecondInstance(argv); } const windows = BrowserWindow3.getAllWindows(); const mainWindow = windows[0]; if (!mainWindow) { return; } if (mainWindow.isMinimized()) { mainWindow.restore(); } mainWindow.focus(); }); return true; } // src/security/index.ts import { shell, ipcMain as ipcMain2 } from "electron"; function setupSecurityPolicy(window, config) { const allowedDomains = config?.allowedExternalDomains ?? []; window.webContents.on("will-navigate", (event, url) => { try { const parsedUrl = new URL(url); if (parsedUrl.protocol !== "file:") { event.preventDefault(); console.warn(`Blocked navigation to: ${url}`); } } catch (error) { event.preventDefault(); console.error(`Blocked malformed URL: ${url}`, error); } }); window.webContents.setWindowOpenHandler(({ url }) => { console.warn(`Blocked new window: ${url}`); return { action: "deny" }; }); ipcMain2.handle("open-external", async (_event, url) => { try { const parsedUrl = new URL(url); if (!["http:", "https:"].includes(parsedUrl.protocol)) { console.warn(`Blocked non-http URL: ${url}`); return false; } if (allowedDomains.length > 0) { const isAllowed = allowedDomains.some( (domain) => parsedUrl.hostname === domain || parsedUrl.hostname.endsWith(`.${domain}`) ); if (!isAllowed) { console.warn(`Blocked non-whitelisted domain: ${url}`); return false; } } await shell.openExternal(url); return true; } catch (error) { console.error(`Failed to open external URL: ${url}`, error); return false; } }); } function createNavigationGuard(window) { window.webContents.on("will-navigate", (event, url) => { try { const parsedUrl = new URL(url); if (parsedUrl.protocol !== "file:") { event.preventDefault(); } } catch { event.preventDefault(); } }); window.webContents.setWindowOpenHandler(() => ({ action: "deny" })); } function validateIpcChannel(channel, allowedChannels) { return allowedChannels.includes(channel); } // src/logger/index.ts import * as fs2 from "fs"; import * as path2 from "path"; import * as os3 from "os"; var LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }; var Logger = class { logDir; logFile; maxFileSize; maxFiles; minLevel; enableConsole; constructor(config) { this.logDir = config.logDir ?? path2.join(os3.homedir(), "AppData", "Roaming", config.appName, "logs"); this.logFile = path2.join(this.logDir, "app.log"); this.maxFileSize = config.maxFileSize ?? 5 * 1024 * 1024; this.maxFiles = config.maxFiles ?? 5; this.minLevel = LOG_LEVELS[config.level ?? "info"]; this.enableConsole = config.console ?? true; fs2.mkdirSync(this.logDir, { recursive: true }); } formatMessage(level, message, meta) { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const metaStr = meta ? ` ${JSON.stringify(meta)}` : ""; return `[${timestamp}] [${level.toUpperCase()}] ${message}${metaStr}`; } write(level, message, meta) { if (LOG_LEVELS[level] < this.minLevel) return; const formatted = this.formatMessage(level, message, meta); if (this.enableConsole) { const consoleFn = level === "error" || level === "fatal" ? console.error : console.warn; consoleFn(formatted); } try { fs2.appendFileSync(this.logFile, `${formatted} `); } catch (error) { console.error("Failed to write log:", error); } } debug(message, meta) { this.write("debug", message, meta); } info(message, meta) { this.write("info", message, meta); } warn(message, meta) { this.write("warn", message, meta); } error(message, meta) { this.write("error", message, meta); } fatal(message, meta) { this.write("fatal", message, meta); } rotateLogs() { try { if (!fs2.existsSync(this.logFile)) return; const stats = fs2.statSync(this.logFile); if (stats.size < this.maxFileSize) return; for (let i = this.maxFiles - 1; i >= 0; i -= 1) { const oldFile = i === 0 ? this.logFile : `${this.logFile}.${i}`; const newFile = `${this.logFile}.${i + 1}`; if (fs2.existsSync(oldFile)) { if (i === this.maxFiles - 1) { fs2.unlinkSync(oldFile); } else { fs2.renameSync(oldFile, newFile); } } } } catch (error) { console.error("Failed to rotate logs:", error); } } getLogDir() { return this.logDir; } }; function createLogger(config) { return new Logger(config); } // src/config/env-loader.ts import * as fs3 from "fs"; import * as path3 from "path"; function loadEnvFile(envPath) { if (!fs3.existsSync(envPath)) { return false; } try { const content = fs3.readFileSync(envPath, "utf8"); const lines = content.split("\n"); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const match = trimmed.match(/^([^=]+)=(.*)$/); if (match) { const [, rawKey = "", rawValue = ""] = match; const key = rawKey.trim(); const value = rawValue.trim(); if (!(key in process.env)) { process.env[key] = value; } } } return true; } catch (error) { console.error(`Failed to load env file: ${envPath}`, error); return false; } } function loadEnvFiles(basePath, isPackaged) { const envFiles = isPackaged ? [path3.join(basePath, ".env.production"), path3.join(basePath, ".env")] : [ path3.join(basePath, ".env.development"), path3.join(basePath, ".env.local"), path3.join(basePath, ".env") ]; for (const envPath of envFiles) { if (loadEnvFile(envPath)) { return envPath; } } return null; } // src/config/paths.ts import * as path4 from "path"; import * as os4 from "os"; import * as fs4 from "fs"; function getAppPaths(appName) { const appData = path4.join(os4.homedir(), "AppData", "Roaming", appName); const paths = { userData: appData, appData, logs: path4.join(appData, "logs"), cache: path4.join(appData, "cache"), config: path4.join(appData, "config") }; const allDirs = [ paths.userData, paths.appData, paths.logs, paths.cache, paths.config ]; for (const dir of allDirs) { fs4.mkdirSync(dir, { recursive: true }); } return paths; } // src/config/app-config-loader.ts import * as fs5 from "fs"; function loadAppConfig(configPath) { const resolvedPath = configPath ?? "./app.config.ts"; if (!fs5.existsSync(resolvedPath)) { console.warn(`Config file not found: ${resolvedPath}, using defaults`); return defaultConfig; } console.warn("Dynamic config loading not supported, import config directly"); return defaultConfig; } // src/crash/index.ts import { app as app3, dialog } from "electron"; import * as fs6 from "fs"; import * as path5 from "path"; var CrashReporter = class { logDir; maxReports; onCrash; appName; initialized = false; constructor(config) { this.appName = config.appName; this.logDir = config.logDir ?? this.getDefaultLogDir(); this.maxReports = config.maxCrashReports ?? 10; this.onCrash = config.onCrash; } getDefaultLogDir() { try { return path5.join(app3.getPath("userData"), "logs"); } catch { return path5.join(process.cwd(), "logs"); } } initialize() { if (this.initialized) return; fs6.mkdirSync(this.logDir, { recursive: true }); process.on("uncaughtException", (error) => { this.handleCrash("uncaughtException", error); }); process.on("unhandledRejection", (reason) => { this.handleCrash("unhandledRejection", reason); }); this.initialized = true; this.log("info", `${this.appName} crash reporter initialized`); } handleCrash(type, error) { const errorMessage = error instanceof Error ? `${error.message} ${error.stack ?? ""}` : String(error); const report = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), type, message: errorMessage, version: app3.isPackaged ? app3.getVersion() : "development", platform: process.platform, arch: process.arch, nodeVersion: process.version, electronVersion: process.versions.electron ?? "unknown" }; const crashFile = path5.join(this.logDir, `crash-${Date.now()}.json`); try { fs6.writeFileSync(crashFile, JSON.stringify(report, null, 2)); } catch (writeError) { console.error("Failed to write crash report:", writeError); } this.log("fatal", `[${type}] ${errorMessage}`); if (this.onCrash) { this.onCrash(report); } if (app3.isReady()) { dialog.showErrorBox( `${this.appName} Error`, `An unexpected error occurred: ${error instanceof Error ? error.message : String(error)} Crash report saved to: ${crashFile} Please restart the application.` ); } app3.exit(1); } log(level, message) { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const logLine = `[${timestamp}] [${level.toUpperCase()}] ${message} `; const logFile = path5.join(this.logDir, "app.log"); try { fs6.appendFileSync(logFile, logLine); } catch (error) { console.error("Failed to write log:", error); } console.error(logLine.trim()); } rotateLogs() { const logFile = path5.join(this.logDir, "app.log"); const maxSize = 5 * 1024 * 1024; const maxFiles = 5; try { if (!fs6.existsSync(logFile)) return; const stats = fs6.statSync(logFile); if (stats.size < maxSize) return; for (let i = maxFiles - 1; i >= 0; i -= 1) { const oldFile = i === 0 ? logFile : `${logFile}.${i}`; const newFile = `${logFile}.${i + 1}`; if (fs6.existsSync(oldFile)) { if (i === maxFiles - 1) { fs6.unlinkSync(oldFile); } else { fs6.renameSync(oldFile, newFile); } } } } catch (error) { console.error("Failed to rotate logs:", error); } } cleanOldCrashReports() { try { const files = fs6.readdirSync(this.logDir).filter((f) => f.startsWith("crash-") && f.endsWith(".json")).map((f) => ({ name: f, path: path5.join(this.logDir, f), time: fs6.statSync(path5.join(this.logDir, f)).mtime.getTime() })).sort((a, b) => b.time - a.time); for (let i = this.maxReports; i < files.length; i += 1) { const file = files[i]; if (!file) { continue; } fs6.unlinkSync(file.path); } } catch (error) { console.error("Failed to clean crash reports:", error); } } }; var crashReporterInstance = null; function getCrashReporter(config) { if (!crashReporterInstance) { crashReporterInstance = new CrashReporter(config); } return crashReporterInstance; } // src/updater/index.ts import { app as app4, BrowserWindow as BrowserWindow5, ipcMain as ipcMain3 } from "electron"; var AutoUpdater = class { autoUpdater = null; updateAvailable = false; updateDownloaded = false; enabled = false; initialize(config) { if (!app4.isPackaged) { console.warn("Auto-updater disabled in development mode"); return false; } try { const { autoUpdater } = __require("electron-updater"); this.autoUpdater = autoUpdater; this.autoUpdater.autoDownload = config?.autoDownload ?? false; this.autoUpdater.autoInstallOnAppQuit = config?.autoInstallOnQuit ?? true; this.setupEventHandlers(); this.enabled = true; const checkInterval = config?.checkInterval ?? 5e3; setTimeout(() => { void this.checkForUpdates(); }, checkInterval); return true; } catch (error) { console.error("Failed to initialize auto-updater:", error); return false; } } setupEventHandlers() { if (!this.autoUpdater) return; this.autoUpdater.on("checking-for-update", () => { this.notifyRenderer({ status: "checking" }); }); this.autoUpdater.on("update-available", (info) => { this.updateAvailable = true; this.notifyRenderer({ status: "available", version: info.version }); }); this.autoUpdater.on("update-not-available", (info) => { this.updateAvailable = false; this.notifyRenderer({ status: "not-available", version: info.version }); }); this.autoUpdater.on("download-progress", (progress) => { this.notifyRenderer({ status: "downloading", percent: progress.percent }); }); this.autoUpdater.on("update-downloaded", (info) => { this.updateDownloaded = true; this.notifyRenderer({ status: "downloaded", version: info.version }); }); this.autoUpdater.on("error", (error) => { this.notifyRenderer({ status: "error", error: error.message }); }); } notifyRenderer(status) { const windows = BrowserWindow5.getAllWindows(); for (const win of windows) { if (!win.isDestroyed()) { win.webContents.send("update-status", status); } } } async checkForUpdates() { if (!this.enabled || !this.autoUpdater) { return { updateAvailable: false }; } try { const result = await this.autoUpdater.checkForUpdates(); return { updateAvailable: this.updateAvailable, version: result?.updateInfo?.version }; } catch (error) { console.error("Failed to check for updates:", error); return { updateAvailable: false }; } } async downloadUpdate() { if (!this.enabled || !this.autoUpdater || !this.updateAvailable) { return false; } try { await this.autoUpdater.downloadUpdate(); return true; } catch (error) { console.error("Failed to download update:", error); return false; } } installUpdate() { if (!this.enabled || !this.autoUpdater || !this.updateDownloaded) { return; } this.autoUpdater.quitAndInstall(false, true); } getStatus() { return { enabled: this.enabled, updateAvailable: this.updateAvailable, updateDownloaded: this.updateDownloaded }; } }; function registerUpdaterHandlers() { const updater = new AutoUpdater(); ipcMain3.handle("get-version", () => ({ version: app4.getVersion(), isPackaged: app4.isPackaged })); ipcMain3.handle("check-for-updates", async () => updater.checkForUpdates()); ipcMain3.handle("download-update", async () => updater.downloadUpdate()); ipcMain3.handle("install-update", () => { updater.installUpdate(); }); ipcMain3.handle("get-update-status", () => updater.getStatus()); } // src/index.ts var VERSION = "0.1.0"; export { AutoUpdater, CrashReporter, KeyManager, LicenseCache, LicenseService, Logger, SecureStorage, VERSION, createLogger, createMainWindow, createNavigationGuard, defaultConfig, defaultTheme, defineConfig, enforceSingleInstance, generateCSSVariables, generateHWID, generateStyleTag, getAppPaths, getCrashReporter, getHWIDComponents, injectTheme, injectThemeOnReady, loadAppConfig, loadEnvFile, loadEnvFiles, presets, registerUpdaterHandlers, registerWindowControls, setupSecurityPolicy, unregisterWindowControls, validateIpcChannel };