UNPKG

@chittyos/core

Version:

ChittyOS Core - Essential package with ID, auth, verification, beacon tracking, and brand components for all ChittyOS applications

546 lines (544 loc) 16 kB
// src/registry/index.ts import { EventEmitter } from "eventemitter3"; import { nanoid as nanoid2 } from "nanoid"; // src/beacon/index.ts import { nanoid } from "nanoid"; import * as os from "os"; import * as fs from "fs"; import { execSync } from "child_process"; var DEFAULT_CONFIG = { endpoint: process.env.CHITTY_BEACON_ENDPOINT || "https://beacon.chitty.cc", interval: parseInt(process.env.CHITTY_BEACON_INTERVAL || "") || 3e5, enabled: process.env.CHITTY_BEACON_DISABLED !== "true", silent: process.env.CHITTY_BEACON_VERBOSE !== "true" }; var config = { ...DEFAULT_CONFIG }; var appInfo = null; var heartbeatInterval = null; function configure(customConfig) { config = { ...config, ...customConfig }; } function detectApp() { const app = { id: generateAppId(), name: detectAppName(), version: detectVersion(), platform: detectPlatform(), environment: process.env.NODE_ENV || "production", hostname: os.hostname(), nodeVersion: process.version, os: `${os.type()} ${os.release()}`, hasClaudeCode: detectClaudeCode(), hasGit: fs.existsSync(".git"), startedAt: (/* @__PURE__ */ new Date()).toISOString(), pid: process.pid, chittyos: { core: "1.0.0", modules: detectChittyModules() } }; if (app.hasGit) { try { app.git = { branch: execSync("git branch --show-current", { encoding: "utf8" }).trim(), commit: execSync("git rev-parse --short HEAD", { encoding: "utf8" }).trim(), remote: execSync("git remote get-url origin", { encoding: "utf8" }).trim() }; } catch (e) { } } return app; } function generateAppId() { if (config.appId) return config.appId; if (process.env.REPL_ID) return `replit-${process.env.REPL_ID}`; if (process.env.GITHUB_REPOSITORY) return `github-${process.env.GITHUB_REPOSITORY.replace("/", "-")}`; if (process.env.VERCEL_URL) return `vercel-${process.env.VERCEL_URL}`; if (process.env.HEROKU_APP_NAME) return `heroku-${process.env.HEROKU_APP_NAME}`; try { const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8")); return `npm-${pkg.name}-${nanoid(8)}`; } catch (e) { return `chitty-${nanoid()}`; } } function detectAppName() { if (config.appName) return config.appName; return process.env.CHITTY_APP_NAME || process.env.REPL_SLUG || process.env.GITHUB_REPOSITORY || process.env.VERCEL_URL || process.env.HEROKU_APP_NAME || process.env.npm_package_name || (() => { try { const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8")); return pkg.name; } catch (e) { return "chittyos-app"; } })(); } function detectVersion() { try { const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8")); return pkg.version; } catch (e) { return "0.0.0"; } } function detectPlatform() { if (process.env.REPL_ID) return "replit"; if (process.env.GITHUB_ACTIONS) return "github-actions"; if (process.env.VERCEL) return "vercel"; if (process.env.NETLIFY) return "netlify"; if (process.env.RENDER) return "render"; if (process.env.HEROKU_APP_NAME) return "heroku"; if (process.env.AWS_LAMBDA_FUNCTION_NAME) return "aws-lambda"; if (process.env.GOOGLE_CLOUD_PROJECT) return "google-cloud"; if (process.env.WEBSITE_INSTANCE_ID) return "azure"; if (process.env.CF_PAGES) return "cloudflare-pages"; if (process.env.CLOUDFLARE_ACCOUNT_ID) return "cloudflare-workers"; return "unknown"; } function detectClaudeCode() { return process.env.CLAUDE_CODE === "true" || fs.existsSync(".claude") || fs.existsSync("CLAUDE.md") || fs.existsSync("claude.json"); } function detectChittyModules() { const modules = []; try { const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8")); const deps = { ...pkg.dependencies, ...pkg.devDependencies }; for (const dep of Object.keys(deps)) { if (dep.startsWith("@chittyos/") || dep.startsWith("@chittycorp/")) { modules.push(dep); } } } catch (e) { } return modules; } async function sendBeacon(event, data) { if (!config.enabled) return; const payload = { ...appInfo, event, timestamp: (/* @__PURE__ */ new Date()).toISOString(), uptime: process.uptime(), ...data }; try { const response = await fetch(`${config.endpoint}/track`, { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": "@chittyos/core/1.0.0" }, body: JSON.stringify(payload) }); if (!config.silent && !response.ok) { console.log(`[ChittyBeacon] Response: ${response.status}`); } } catch (error) { if (!config.silent) { console.log(`[ChittyBeacon] Error: ${error.message}`); } } } function init(customConfig) { if (customConfig) { configure(customConfig); } if (!config.enabled) { if (!config.silent) { console.log("[ChittyBeacon] Disabled"); } return; } appInfo = detectApp(); sendBeacon("startup"); heartbeatInterval = setInterval(() => { sendBeacon("heartbeat"); }, config.interval); heartbeatInterval.unref(); const shutdown = () => { sendBeacon("shutdown"); }; process.once("exit", shutdown); process.once("SIGINT", shutdown); process.once("SIGTERM", shutdown); if (!config.silent) { console.log(`[ChittyBeacon] Tracking ${appInfo.name} on ${appInfo.platform}`); } } if (process.env.NODE_ENV !== "test") { init(); } // src/registry/index.ts var ServiceRegistry = class extends EventEmitter { config; services = /* @__PURE__ */ new Map(); connections = /* @__PURE__ */ new Map(); healthStatus = /* @__PURE__ */ new Map(); healthCheckTimers = /* @__PURE__ */ new Map(); discoveryTimer = null; constructor(config2 = {}) { super(); this.config = { discoveryEndpoint: config2.discoveryEndpoint || process.env.CHITTY_REGISTRY_ENDPOINT || "https://registry.chitty.cc", healthCheckInterval: config2.healthCheckInterval || 3e4, connectionTimeout: config2.connectionTimeout || 1e4, maxRetries: config2.maxRetries || 3, enableAutoDiscovery: config2.enableAutoDiscovery ?? true }; if (this.config.enableAutoDiscovery) { this.startAutoDiscovery(); } } /** * Register a service endpoint */ registerService(service) { const registeredService = { ...service, id: service.name + "_" + nanoid2(8) }; try { const url = new URL(service.url); registeredService.protocol = url.protocol.replace(":", ""); registeredService.host = url.hostname; registeredService.port = url.port ? parseInt(url.port) : void 0; registeredService.path = url.pathname !== "/" ? url.pathname : void 0; } catch (error) { console.warn("[Registry] Invalid URL for service:", service.name); } this.services.set(registeredService.id, registeredService); this.emit("service:registered", registeredService); this.startHealthCheck(registeredService.id); sendBeacon("registry_service_registered", { serviceId: registeredService.id, name: registeredService.name, type: registeredService.type }); return registeredService; } /** * Unregister a service */ unregisterService(serviceId) { const service = this.services.get(serviceId); if (!service) { return false; } this.stopHealthCheck(serviceId); for (const [connId, conn] of this.connections) { if (conn.serviceId === serviceId) { this.disconnectConnection(connId); } } this.services.delete(serviceId); this.healthStatus.delete(serviceId); this.emit("service:unregistered", serviceId); return true; } /** * Get service by ID or name */ getService(idOrName) { if (this.services.has(idOrName)) { return this.services.get(idOrName); } for (const service of this.services.values()) { if (service.name === idOrName) { return service; } } return void 0; } /** * Get all services of a specific type */ getServicesByType(type2) { return Array.from(this.services.values()).filter((s) => s.type === type2); } /** * Establish connection to a service */ async connectToService(serviceId, chittyId, metadata) { const service = this.services.get(serviceId); if (!service) { throw new Error(`Service ${serviceId} not found`); } const connection = { id: `conn_${nanoid2()}`, serviceId, chittyId, status: "connecting", metadata }; this.connections.set(connection.id, connection); try { const startTime = Date.now(); await this.testConnection(service); const latency = Date.now() - startTime; connection.status = "connected"; connection.establishedAt = (/* @__PURE__ */ new Date()).toISOString(); connection.latency = latency; this.connections.set(connection.id, connection); this.emit("connection:established", connection); sendBeacon("registry_connection_established", { serviceId, chittyId, latency }); return connection; } catch (error) { connection.status = "error"; connection.error = error.message; this.connections.set(connection.id, connection); throw error; } } /** * Disconnect a connection */ disconnectConnection(connectionId) { const connection = this.connections.get(connectionId); if (!connection) { return false; } connection.status = "disconnected"; this.emit("connection:lost", connection); this.connections.delete(connectionId); return true; } /** * Get connection status */ getConnection(connectionId) { return this.connections.get(connectionId); } /** * Get all connections for a ChittyID */ getConnectionsByChittyId(chittyId) { return Array.from(this.connections.values()).filter((c) => c.chittyId === chittyId); } /** * Test connection to a service */ async testConnection(service) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), this.config.connectionTimeout); try { const response = await fetch(service.url, { method: "HEAD", signal: controller.signal }); if (!response.ok && response.status !== 405) { throw new Error(`Service returned ${response.status}`); } } catch (error) { if (error.name === "AbortError") { throw new Error("Connection timeout"); } throw error; } finally { clearTimeout(timeout); } } /** * Perform health check on a service */ async performHealthCheck(serviceId) { const service = this.services.get(serviceId); if (!service) { return; } const health = { serviceId, status: "unknown", lastCheckAt: (/* @__PURE__ */ new Date()).toISOString(), checks: [] }; try { const startTime = Date.now(); await this.testConnection(service); const responseTime = Date.now() - startTime; health.responseTime = responseTime; health.status = responseTime < 1e3 ? "healthy" : "degraded"; health.checks.push({ name: "connectivity", status: "pass", message: `Response time: ${responseTime}ms` }); try { const healthUrl = new URL(service.url); healthUrl.pathname = "/health"; const healthResponse = await fetch(healthUrl.toString(), { signal: AbortSignal.timeout(5e3) }); if (healthResponse.ok) { const healthData = await healthResponse.json(); if (healthData.status) { health.status = healthData.status; } if (healthData.checks) { health.checks.push(...healthData.checks); } } } catch (error) { } this.healthStatus.set(serviceId, health); if (health.status === "healthy") { this.emit("service:healthy", serviceId); } else if (health.status === "unhealthy") { this.emit("service:unhealthy", serviceId, "Health check failed"); } } catch (error) { health.status = "unhealthy"; health.checks.push({ name: "connectivity", status: "fail", message: error.message }); this.healthStatus.set(serviceId, health); this.emit("service:unhealthy", serviceId, error.message); } } /** * Start health checks for a service */ startHealthCheck(serviceId) { this.performHealthCheck(serviceId); const timer = setInterval(() => { this.performHealthCheck(serviceId); }, this.config.healthCheckInterval); this.healthCheckTimers.set(serviceId, timer); } /** * Stop health checks for a service */ stopHealthCheck(serviceId) { const timer = this.healthCheckTimers.get(serviceId); if (timer) { clearInterval(timer); this.healthCheckTimers.delete(serviceId); } } /** * Auto-discover services from registry endpoint */ async discoverServices() { if (!this.config.discoveryEndpoint) { return; } try { const response = await fetch(`${this.config.discoveryEndpoint}/discover`); if (response.ok) { const services = await response.json(); for (const service of services) { if (!this.getService(service.id)) { this.registerService(service); } } this.emit("discovery:update", services); } } catch (error) { console.error("[Registry] Discovery failed:", error); } } /** * Start auto-discovery */ startAutoDiscovery() { this.discoverServices(); this.discoveryTimer = setInterval(() => { this.discoverServices(); }, 6e4); } /** * Stop auto-discovery */ stopAutoDiscovery() { if (this.discoveryTimer) { clearInterval(this.discoveryTimer); this.discoveryTimer = null; } } /** * Get service health status */ getHealth(serviceId) { return this.healthStatus.get(serviceId); } /** * Get all registered services */ getAllServices() { return Array.from(this.services.values()); } /** * Get all active connections */ getAllConnections() { return Array.from(this.connections.values()); } /** * Get registry statistics */ getStats() { const healthyServices = Array.from(this.healthStatus.values()).filter((h) => h.status === "healthy").length; const unhealthyServices = Array.from(this.healthStatus.values()).filter((h) => h.status === "unhealthy").length; const activeConnections = Array.from(this.connections.values()).filter((c) => c.status === "connected").length; return { totalServices: this.services.size, healthyServices, unhealthyServices, totalConnections: this.connections.size, activeConnections }; } /** * Cleanup and shutdown */ shutdown() { for (const serviceId of this.healthCheckTimers.keys()) { this.stopHealthCheck(serviceId); } this.stopAutoDiscovery(); this.services.clear(); this.connections.clear(); this.healthStatus.clear(); } }; var registryInstance = null; function getRegistry(config2) { if (!registryInstance) { registryInstance = new ServiceRegistry(config2); } return registryInstance; } function registerService(service) { return getRegistry().registerService(service); } function connectToService(serviceId, chittyId, metadata) { return getRegistry().connectToService(serviceId, chittyId, metadata); } function getService(idOrName) { return getRegistry().getService(idOrName); } function getAllServices() { return getRegistry().getAllServices(); } var registry_default = { getRegistry, registerService, connectToService, getService, getAllServices, ServiceRegistry }; export { connectToService, registry_default as default, getAllServices, getRegistry, getService, registerService }; //# sourceMappingURL=index.mjs.map