electron-license-kit
Version:
Secure licensing, storage, and utilities for Electron apps with Supabase
1,332 lines (1,299 loc) • 39.2 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
AutoUpdater: () => AutoUpdater,
CrashReporter: () => CrashReporter,
KeyManager: () => KeyManager,
LicenseCache: () => LicenseCache,
LicenseService: () => LicenseService,
Logger: () => Logger,
SecureStorage: () => SecureStorage,
VERSION: () => VERSION,
createLogger: () => createLogger,
createMainWindow: () => createMainWindow,
createNavigationGuard: () => createNavigationGuard,
defaultConfig: () => defaultConfig,
defaultTheme: () => defaultTheme,
defineConfig: () => defineConfig,
enforceSingleInstance: () => enforceSingleInstance,
generateCSSVariables: () => generateCSSVariables,
generateHWID: () => generateHWID,
generateStyleTag: () => generateStyleTag,
getAppPaths: () => getAppPaths,
getCrashReporter: () => getCrashReporter,
getHWIDComponents: () => getHWIDComponents,
injectTheme: () => injectTheme,
injectThemeOnReady: () => injectThemeOnReady,
loadAppConfig: () => loadAppConfig,
loadEnvFile: () => loadEnvFile,
loadEnvFiles: () => loadEnvFiles,
presets: () => presets,
registerUpdaterHandlers: () => registerUpdaterHandlers,
registerWindowControls: () => registerWindowControls,
setupSecurityPolicy: () => setupSecurityPolicy,
unregisterWindowControls: () => unregisterWindowControls,
validateIpcChannel: () => validateIpcChannel
});
module.exports = __toCommonJS(index_exports);
// 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
var crypto = __toESM(require("crypto"));
var os = __toESM(require("os"));
var import_node_machine_id = require("node-machine-id");
function getMachineId() {
try {
return (0, import_node_machine_id.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
var crypto2 = __toESM(require("crypto"));
var fs = __toESM(require("fs"));
var path = __toESM(require("path"));
var os2 = __toESM(require("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
var import_supabase_js = require("@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 = (0, import_supabase_js.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
var import_electron = require("electron");
function createMainWindow(config) {
const window = new import_electron.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 && !import_electron.app.isPackaged) {
window.webContents.openDevTools();
}
return window;
}
// src/window/window-controls.ts
var import_electron2 = require("electron");
function registerWindowControls(window) {
import_electron2.ipcMain.on("window-minimize", () => {
if (window && !window.isDestroyed()) {
window.minimize();
}
});
import_electron2.ipcMain.on("window-maximize", () => {
if (window && !window.isDestroyed()) {
if (window.isMaximized()) {
window.unmaximize();
} else {
window.maximize();
}
}
});
import_electron2.ipcMain.on("window-close", () => {
if (window && !window.isDestroyed()) {
window.close();
}
});
}
function unregisterWindowControls() {
import_electron2.ipcMain.removeAllListeners("window-minimize");
import_electron2.ipcMain.removeAllListeners("window-maximize");
import_electron2.ipcMain.removeAllListeners("window-close");
}
// src/window/single-instance.ts
var import_electron3 = require("electron");
function enforceSingleInstance(config) {
const gotLock = import_electron3.app.requestSingleInstanceLock();
if (!gotLock) {
return false;
}
import_electron3.app.on("second-instance", (_event, argv) => {
if (config?.onSecondInstance) {
config.onSecondInstance(argv);
}
const windows = import_electron3.BrowserWindow.getAllWindows();
const mainWindow = windows[0];
if (!mainWindow) {
return;
}
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
mainWindow.focus();
});
return true;
}
// src/security/index.ts
var import_electron4 = require("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" };
});
import_electron4.ipcMain.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 import_electron4.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
var fs2 = __toESM(require("fs"));
var path2 = __toESM(require("path"));
var os3 = __toESM(require("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
var fs3 = __toESM(require("fs"));
var path3 = __toESM(require("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
var path4 = __toESM(require("path"));
var os4 = __toESM(require("os"));
var fs4 = __toESM(require("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
var fs5 = __toESM(require("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
var import_electron5 = require("electron");
var fs6 = __toESM(require("fs"));
var path5 = __toESM(require("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(import_electron5.app.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: import_electron5.app.isPackaged ? import_electron5.app.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 (import_electron5.app.isReady()) {
import_electron5.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.`
);
}
import_electron5.app.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
var import_electron6 = require("electron");
var AutoUpdater = class {
autoUpdater = null;
updateAvailable = false;
updateDownloaded = false;
enabled = false;
initialize(config) {
if (!import_electron6.app.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 = import_electron6.BrowserWindow.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();
import_electron6.ipcMain.handle("get-version", () => ({
version: import_electron6.app.getVersion(),
isPackaged: import_electron6.app.isPackaged
}));
import_electron6.ipcMain.handle("check-for-updates", async () => updater.checkForUpdates());
import_electron6.ipcMain.handle("download-update", async () => updater.downloadUpdate());
import_electron6.ipcMain.handle("install-update", () => {
updater.installUpdate();
});
import_electron6.ipcMain.handle("get-update-status", () => updater.getStatus());
}
// src/index.ts
var VERSION = "0.1.0";
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
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
});