feat: add universal adapters for browser compatibility

- Create universal adapters for cross-platform support (browser/Node/serverless)
- Replace Node.js-specific imports with universal implementations
- Add OPFS support for browser persistent storage
- Maintain same BrainyData interface across all environments
- Enable real Brainy usage in browser console UI
- Keep package size optimized (no bloat)

Universal adapters in /src/universal/:
- uuid.ts: Cross-platform UUID generation
- crypto.ts: Browser/Node crypto operations
- fs.ts: OPFS/FileSystem/Memory storage adapter
- path.ts: Universal path operations
- events.ts: EventEmitter compatibility layer

This enables 'write once, run anywhere' for Brainy while maintaining
the exact same API. No breaking changes to existing code.
This commit is contained in:
David Snelling 2025-08-08 15:22:38 -07:00
parent 6e8869ecb7
commit 93804be354
31 changed files with 2120 additions and 2028 deletions

View file

@ -5,7 +5,7 @@ import {
AugmentationResponse,
WebSocketConnection
} from '../types/augmentations.js'
import { v4 as uuidv4 } from 'uuid'
import { v4 as uuidv4 } from '../universal/uuid.js'
/**
* Base class for conduit augmentations that provide data synchronization between Brainy instances

View file

@ -8,8 +8,8 @@
import { ISenseAugmentation, AugmentationResponse } from '../types/augmentations.js'
import { BrainyData } from '../brainyData.js'
import { NounType, VerbType } from '../types/graphTypes.js'
import * as fs from 'fs/promises'
import * as path from 'path'
import * as fs from '../universal/fs.js'
import * as path from '../universal/path.js'
// Cortex Analysis Types
export interface CortexAnalysisResult {

View file

@ -14,7 +14,7 @@ import {
WebSocketConnection
} from '../types/augmentations.js'
import { WebSocketConduitAugmentation } from './conduitAugmentations.js'
import { v4 as uuidv4 } from 'uuid'
import { v4 as uuidv4 } from '../universal/uuid.js'
import { BrainyDataInterface } from '../types/brainyDataInterface.js'
/**

View file

@ -3,7 +3,7 @@
* Main class that provides the vector database functionality
*/
import { v4 as uuidv4 } from 'uuid'
import { v4 as uuidv4 } from './universal/uuid.js'
import { HNSWIndex } from './hnsw/hnswIndex.js'
import {
HNSWIndexOptimized,
@ -6071,7 +6071,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
// Configure HNSW with disk-based storage when a storage adapter is provided
const hnswConfig = data.hnswIndex.config || {}
if (this.storage) {
hnswConfig.useDiskBasedIndex = true
;(hnswConfig as any).useDiskBasedIndex = true
}
this.index = new HNSWIndexOptimized(

View file

@ -0,0 +1,35 @@
/**
* Minimal Browser Framework Entry Point for Brainy
* Core MIT open source functionality only - no enterprise features
* Optimized for browser usage with all dependencies bundled
*/
import { BrainyData } from './brainyData.js'
import { VerbType, NounType } from './types/graphTypes.js'
/**
* Create a BrainyData instance optimized for browser usage
* Auto-detects environment and selects optimal storage and settings
*/
export async function createBrowserBrainyData(config = {}) {
// BrainyData already has environment detection and will automatically:
// - Use OPFS storage in browsers with fallback to Memory
// - Use FileSystem storage in Node.js
// - Request persistent storage when appropriate
const browserConfig = {
storage: {
requestPersistentStorage: true // Request persistent storage for better performance
},
...config
}
const brainyData = new BrainyData(browserConfig)
await brainyData.init()
return brainyData
}
// Re-export core types and classes for browser use
export { VerbType, NounType, BrainyData }
// Default export for easy importing
export default createBrowserBrainyData

View file

@ -6,8 +6,8 @@
*/
import { BrainyData } from '../brainyData.js'
import * as fs from 'fs/promises'
import * as path from 'path'
import * as fs from '../universal/fs.js'
import * as path from '../universal/path.js'
// @ts-ignore
import chalk from 'chalk'
// @ts-ignore

View file

@ -8,7 +8,7 @@ import { BrainyData } from '../brainyData.js'
import { BrainyChat } from '../chat/brainyChat.js'
import { PerformanceMonitor } from './performanceMonitor.js'
import { HealthCheck } from './healthCheck.js'
import { LicensingSystem } from './licensingSystem.js'
// Licensing system moved to quantum-vault
import * as readline from 'readline'
import * as fs from 'fs/promises'
import * as path from 'path'
@ -81,7 +81,7 @@ export class Cortex {
private chatInstance?: BrainyChat
private performanceMonitor?: PerformanceMonitor
private healthCheck?: HealthCheck
private licensingSystem?: LicensingSystem
// private licensingSystem?: LicensingSystem // Moved to quantum-vault
private configPath: string
private config: CortexConfig
private encryptionKey?: Buffer
@ -1734,8 +1734,8 @@ export class Cortex {
this.healthCheck = new HealthCheck(this.brainy)
// Initialize licensing system
this.licensingSystem = new LicensingSystem()
await this.licensingSystem.initialize()
// Licensing system moved to quantum-vault for premium features
// Open source version has full functionality available
}
private async saveConfig(): Promise<void> {

View file

@ -1,623 +0,0 @@
/**
* Brainy Premium Licensing System - Atomic Age Revenue Engine
*
* 🧠 Manages premium augmentation licenses and subscriptions
* 1950s retro sci-fi themed licensing with atomic age aesthetics
* 🚀 Scalable license validation for premium features
*/
// @ts-ignore
import chalk from 'chalk'
// @ts-ignore
import boxen from 'boxen'
// @ts-ignore
import ora from 'ora'
import * as fs from 'fs/promises'
import * as path from 'path'
import * as crypto from 'crypto'
export interface License {
id: string
type: 'premium' | 'enterprise' | 'trial'
product: string // e.g., 'salesforce-connector', 'slack-connector'
tier: 'basic' | 'professional' | 'enterprise'
status: 'active' | 'expired' | 'suspended' | 'trial'
issuedTo: string // Customer identifier
issuedAt: string // ISO timestamp
expiresAt: string // ISO timestamp
features: string[] // Array of enabled features
limits: {
apiCallsPerMonth?: number
dataVolumeGB?: number
concurrentConnections?: number
customConnectors?: number
}
metadata: {
customerName: string
customerEmail: string
subscriptionId?: string
paymentStatus?: 'active' | 'past_due' | 'canceled'
}
signature: string // Cryptographic signature for validation
}
export interface LicenseValidationResult {
valid: boolean
license?: License
reason?: string
expiresIn?: number // Days until expiration
usage?: {
apiCalls: number
dataUsed: number
connectionsUsed: number
}
}
export interface PremiumFeature {
id: string
name: string
description: string
category: 'connector' | 'intelligence' | 'enterprise'
requiredTier: 'basic' | 'professional' | 'enterprise'
monthlyPrice: number
yearlyPrice: number
trialDays: number
}
/**
* Premium Licensing and Revenue Management System
*/
export class LicensingSystem {
private licensePath: string
private premiumFeatures: Map<string, PremiumFeature> = new Map()
private activeLicenses: Map<string, License> = new Map()
private colors = {
primary: chalk.hex('#3A5F4A'),
success: chalk.hex('#2D4A3A'),
warning: chalk.hex('#D67441'),
error: chalk.hex('#B85C35'),
info: chalk.hex('#4A6B5A'),
dim: chalk.hex('#8A9B8A'),
highlight: chalk.hex('#E88B5A'),
accent: chalk.hex('#F5E6D3'),
brain: chalk.hex('#E88B5A'),
premium: chalk.hex('#FFD700'), // Gold for premium features
enterprise: chalk.hex('#C0C0C0') // Silver for enterprise
}
private emojis = {
brain: '🧠',
atom: '⚛️',
premium: '👑',
enterprise: '🏢',
trial: '⏰',
lock: '🔒',
unlock: '🔓',
key: '🗝️',
shield: '🛡️',
check: '✅',
cross: '❌',
warning: '⚠️',
sparkle: '✨',
rocket: '🚀',
money: '💰',
card: '💳',
gear: '⚙️'
}
constructor() {
this.licensePath = path.join(process.cwd(), '.cortex', 'licenses.json')
this.initializePremiumFeatures()
}
/**
* Initialize the licensing system
*/
async initialize(): Promise<void> {
await this.loadLicenses()
await this.validateAllLicenses()
}
/**
* Check if a premium feature is licensed and available
*/
async validateFeature(featureId: string, customerId?: string): Promise<LicenseValidationResult> {
const feature = this.premiumFeatures.get(featureId)
if (!feature) {
return {
valid: false,
reason: `Feature '${featureId}' not found`
}
}
// Find applicable license
let applicableLicense: License | undefined
for (const license of this.activeLicenses.values()) {
if (license.features.includes(featureId) && license.status === 'active') {
if (!customerId || license.issuedTo === customerId) {
applicableLicense = license
break
}
}
}
if (!applicableLicense) {
return {
valid: false,
reason: 'No valid license found for this feature'
}
}
// Check expiration
const now = new Date()
const expiryDate = new Date(applicableLicense.expiresAt)
const expiresIn = Math.ceil((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
if (expiresIn <= 0) {
return {
valid: false,
license: applicableLicense,
reason: 'License has expired',
expiresIn: 0
}
}
// Validate signature
if (!this.validateLicenseSignature(applicableLicense)) {
return {
valid: false,
license: applicableLicense,
reason: 'License signature is invalid'
}
}
return {
valid: true,
license: applicableLicense,
expiresIn,
usage: await this.getCurrentUsage(applicableLicense.id)
}
}
/**
* Display premium features catalog
*/
async displayFeatureCatalog(): Promise<void> {
console.log(boxen(
`${this.emojis.premium} ${this.colors.brain('BRAINY PREMIUM CATALOG')} ${this.emojis.atom}\n` +
`${this.colors.dim('Unlock the full potential of your atomic-age vector + graph database')}`,
{ padding: 1, borderStyle: 'double', borderColor: '#FFD700', width: 80 }
))
console.log('\n' + this.colors.brain(`${this.emojis.rocket} API CONNECTORS (Premium)`))
const connectors = Array.from(this.premiumFeatures.values())
.filter(f => f.category === 'connector')
connectors.forEach(feature => {
const priceMonthly = this.colors.premium(`$${feature.monthlyPrice}/month`)
const priceYearly = this.colors.success(`$${feature.yearlyPrice}/year`)
const savings = Math.round(((feature.monthlyPrice * 12 - feature.yearlyPrice) / (feature.monthlyPrice * 12)) * 100)
console.log(
`\n ${this.emojis.gear} ${this.colors.highlight(feature.name)}\n` +
` ${this.colors.dim(feature.description)}\n` +
` ${this.colors.accent('Pricing:')} ${priceMonthly} | ${priceYearly} ${this.colors.success(`(Save ${savings}%)`)} | ${this.colors.info(`${feature.trialDays} days free trial`)}`
)
})
console.log('\n' + this.colors.brain(`${this.emojis.sparkle} INTELLIGENCE FEATURES (Premium)`))
const intelligence = Array.from(this.premiumFeatures.values())
.filter(f => f.category === 'intelligence')
intelligence.forEach(feature => {
console.log(
`\n ${this.emojis.brain} ${this.colors.highlight(feature.name)}\n` +
` ${this.colors.dim(feature.description)}\n` +
` ${this.colors.accent('Tier:')} ${this.colors.premium(feature.requiredTier)} | ${this.colors.accent('Trial:')} ${this.colors.info(`${feature.trialDays} days`)}`
)
})
console.log('\n' + this.colors.enterprise(`${this.emojis.enterprise} ENTERPRISE FEATURES`))
const enterprise = Array.from(this.premiumFeatures.values())
.filter(f => f.category === 'enterprise')
enterprise.forEach(feature => {
console.log(
`\n ${this.emojis.shield} ${this.colors.highlight(feature.name)}\n` +
` ${this.colors.dim(feature.description)}\n` +
` ${this.colors.accent('Contact sales for pricing')}`
)
})
console.log('\n' + boxen(
`${this.emojis.money} ${this.colors.premium('START YOUR ATOMIC AGE TRANSFORMATION')}\n\n` +
`${this.colors.accent('◆')} Free trial for all premium features\n` +
`${this.colors.accent('◆')} No credit card required to start\n` +
`${this.colors.accent('◆')} Cancel anytime, no questions asked\n\n` +
`${this.colors.dim('Visit https://soulcraft-research.com/brainy/premium to get started')}`,
{ padding: 1, borderStyle: 'round', borderColor: '#FFD700' }
))
}
/**
* Activate a trial license for a feature
*/
async startTrial(featureId: string, customerInfo: { name: string, email: string }): Promise<License | null> {
const feature = this.premiumFeatures.get(featureId)
if (!feature) {
console.log(this.colors.error(`Feature '${featureId}' not found`))
return null
}
console.log(boxen(
`${this.emojis.trial} ${this.colors.brain('ATOMIC TRIAL ACTIVATION')} ${this.emojis.atom}\n\n` +
`${this.colors.accent('◆')} ${this.colors.dim('Feature:')} ${this.colors.highlight(feature.name)}\n` +
`${this.colors.accent('◆')} ${this.colors.dim('Trial Duration:')} ${this.colors.success(feature.trialDays + ' days')}\n` +
`${this.colors.accent('◆')} ${this.colors.dim('Customer:')} ${this.colors.primary(customerInfo.name)}`,
{ padding: 1, borderStyle: 'round', borderColor: '#E88B5A' }
))
const now = new Date()
const expiryDate = new Date(now.getTime() + (feature.trialDays * 24 * 60 * 60 * 1000))
const license: License = {
id: this.generateLicenseId(),
type: 'trial',
product: featureId,
tier: 'basic',
status: 'active',
issuedTo: customerInfo.email,
issuedAt: now.toISOString(),
expiresAt: expiryDate.toISOString(),
features: [featureId],
limits: {
apiCallsPerMonth: 1000,
dataVolumeGB: 10,
concurrentConnections: 3,
customConnectors: 0
},
metadata: {
customerName: customerInfo.name,
customerEmail: customerInfo.email,
paymentStatus: 'active'
},
signature: ''
}
// Generate signature
license.signature = this.generateLicenseSignature(license)
// Store license
this.activeLicenses.set(license.id, license)
await this.saveLicenses()
console.log(this.colors.success(`\n${this.emojis.sparkle} Trial activated! License ID: ${license.id}`))
console.log(this.colors.dim(`Expires: ${expiryDate.toLocaleDateString()}`))
return license
}
/**
* Check license status and display information
*/
async checkLicenseStatus(licenseId?: string): Promise<void> {
if (licenseId) {
const license = this.activeLicenses.get(licenseId)
if (!license) {
console.log(this.colors.error(`License '${licenseId}' not found`))
return
}
await this.displayLicenseDetails(license)
} else {
await this.displayAllLicenses()
}
}
/**
* Initialize premium features catalog
*/
private initializePremiumFeatures(): void {
// API Connectors
this.premiumFeatures.set('salesforce-connector', {
id: 'salesforce-connector',
name: 'Salesforce Connector',
description: 'Real-time sync with Salesforce CRM data, contacts, opportunities, and accounts',
category: 'connector',
requiredTier: 'professional',
monthlyPrice: 49,
yearlyPrice: 490, // 2 months free
trialDays: 14
})
this.premiumFeatures.set('slack-connector', {
id: 'slack-connector',
name: 'Slack Integration',
description: 'Import Slack channels, messages, and team data for intelligent search',
category: 'connector',
requiredTier: 'basic',
monthlyPrice: 29,
yearlyPrice: 290,
trialDays: 7
})
this.premiumFeatures.set('notion-connector', {
id: 'notion-connector',
name: 'Notion Workspace Sync',
description: 'Sync Notion pages, databases, and documentation for semantic search',
category: 'connector',
requiredTier: 'professional',
monthlyPrice: 39,
yearlyPrice: 390,
trialDays: 14
})
this.premiumFeatures.set('hubspot-connector', {
id: 'hubspot-connector',
name: 'HubSpot CRM Integration',
description: 'Connect HubSpot contacts, deals, and marketing data',
category: 'connector',
requiredTier: 'professional',
monthlyPrice: 59,
yearlyPrice: 590,
trialDays: 14
})
this.premiumFeatures.set('jira-connector', {
id: 'jira-connector',
name: 'Jira Project Sync',
description: 'Import Jira tickets, projects, and development workflows',
category: 'connector',
requiredTier: 'basic',
monthlyPrice: 34,
yearlyPrice: 340,
trialDays: 10
})
this.premiumFeatures.set('asana-connector', {
id: 'asana-connector',
name: 'Asana Project Integration',
description: 'Sync Asana tasks, projects, teams, and milestone data for intelligent project insights',
category: 'connector',
requiredTier: 'professional',
monthlyPrice: 44,
yearlyPrice: 440,
trialDays: 14
})
// Intelligence Features
this.premiumFeatures.set('auto-insights', {
id: 'auto-insights',
name: 'Proactive AI Insights',
description: 'Automatic pattern detection and intelligent recommendations',
category: 'intelligence',
requiredTier: 'professional',
monthlyPrice: 79,
yearlyPrice: 790,
trialDays: 21
})
this.premiumFeatures.set('smart-autocomplete', {
id: 'smart-autocomplete',
name: 'Intelligent Auto-Complete',
description: 'Context-aware search suggestions and query completion',
category: 'intelligence',
requiredTier: 'basic',
monthlyPrice: 19,
yearlyPrice: 190,
trialDays: 14
})
// Enterprise Features
this.premiumFeatures.set('advanced-security', {
id: 'advanced-security',
name: 'Advanced Security Suite',
description: 'Enterprise-grade encryption, audit logs, and compliance features',
category: 'enterprise',
requiredTier: 'enterprise',
monthlyPrice: 199,
yearlyPrice: 1990,
trialDays: 30
})
this.premiumFeatures.set('custom-connectors', {
id: 'custom-connectors',
name: 'Custom Connector Development',
description: 'Build and deploy custom API connectors for your specific needs',
category: 'enterprise',
requiredTier: 'enterprise',
monthlyPrice: 299,
yearlyPrice: 2990,
trialDays: 30
})
}
/**
* Load licenses from storage
*/
private async loadLicenses(): Promise<void> {
try {
const data = await fs.readFile(this.licensePath, 'utf8')
const licenses: License[] = JSON.parse(data)
for (const license of licenses) {
this.activeLicenses.set(license.id, license)
}
} catch (error) {
// File doesn't exist or is invalid - start fresh
this.activeLicenses.clear()
}
}
/**
* Save licenses to storage
*/
private async saveLicenses(): Promise<void> {
const dir = path.dirname(this.licensePath)
await fs.mkdir(dir, { recursive: true })
const licenses = Array.from(this.activeLicenses.values())
await fs.writeFile(this.licensePath, JSON.stringify(licenses, null, 2))
}
/**
* Validate all loaded licenses
*/
private async validateAllLicenses(): Promise<void> {
const now = new Date()
const expiredLicenses: string[] = []
for (const [id, license] of this.activeLicenses) {
const expiryDate = new Date(license.expiresAt)
if (expiryDate <= now) {
license.status = 'expired'
expiredLicenses.push(id)
} else if (!this.validateLicenseSignature(license)) {
license.status = 'suspended'
expiredLicenses.push(id)
}
}
if (expiredLicenses.length > 0) {
await this.saveLicenses()
}
}
/**
* Generate cryptographic signature for license
*/
private generateLicenseSignature(license: License): string {
const data = `${license.id}:${license.type}:${license.product}:${license.issuedTo}:${license.expiresAt}`
const secret = process.env.BRAINY_LICENSE_SECRET || 'default-secret-key-change-in-production'
return crypto.createHmac('sha256', secret)
.update(data)
.digest('hex')
}
/**
* Validate license signature
*/
private validateLicenseSignature(license: License): boolean {
const expectedSignature = this.generateLicenseSignature(license)
return crypto.timingSafeEqual(
Buffer.from(license.signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
)
}
/**
* Generate unique license ID
*/
private generateLicenseId(): string {
return 'lic_' + crypto.randomBytes(16).toString('hex')
}
/**
* Get current usage statistics for a license
*/
private async getCurrentUsage(licenseId: string): Promise<{ apiCalls: number, dataUsed: number, connectionsUsed: number }> {
// Placeholder - would track actual usage
return {
apiCalls: Math.floor(Math.random() * 500),
dataUsed: Math.floor(Math.random() * 5),
connectionsUsed: Math.floor(Math.random() * 3)
}
}
/**
* Display detailed license information
*/
private async displayLicenseDetails(license: License): Promise<void> {
const feature = this.premiumFeatures.get(license.product)
const now = new Date()
const expiryDate = new Date(license.expiresAt)
const daysLeft = Math.ceil((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
const usage = await this.getCurrentUsage(license.id)
const statusColor = license.status === 'active' ? this.colors.success :
license.status === 'trial' ? this.colors.warning :
license.status === 'expired' ? this.colors.error :
this.colors.dim
const statusIcon = license.status === 'active' ? this.emojis.check :
license.status === 'trial' ? this.emojis.trial :
license.status === 'expired' ? this.emojis.cross :
this.emojis.warning
console.log(boxen(
`${this.emojis.key} ${this.colors.brain('LICENSE DETAILS')} ${this.emojis.atom}\n\n` +
`${this.colors.accent('License ID:')} ${this.colors.primary(license.id)}\n` +
`${this.colors.accent('Product:')} ${this.colors.highlight(feature?.name || license.product)}\n` +
`${this.colors.accent('Type:')} ${this.colors.premium(license.type)}\n` +
`${this.colors.accent('Status:')} ${statusIcon} ${statusColor(license.status)}\n` +
`${this.colors.accent('Customer:')} ${this.colors.primary(license.metadata.customerName)}\n` +
`${this.colors.accent('Expires:')} ${daysLeft > 0 ? this.colors.success(`${daysLeft} days`) : this.colors.error('Expired')}`,
{ padding: 1, borderStyle: 'round', borderColor: license.status === 'active' ? '#2D4A3A' : '#D67441' }
))
// Usage statistics
if (license.status === 'active' || license.status === 'trial') {
console.log('\n' + this.colors.brain(`${this.emojis.gear} USAGE STATISTICS`))
const apiUsage = license.limits.apiCallsPerMonth ?
`${usage.apiCalls}/${license.limits.apiCallsPerMonth}` :
usage.apiCalls.toString()
const dataUsage = license.limits.dataVolumeGB ?
`${usage.dataUsed}GB/${license.limits.dataVolumeGB}GB` :
`${usage.dataUsed}GB`
console.log(` ${this.colors.accent('API Calls:')} ${this.colors.primary(apiUsage)}`)
console.log(` ${this.colors.accent('Data Used:')} ${this.colors.primary(dataUsage)}`)
console.log(` ${this.colors.accent('Connections:')} ${this.colors.primary(usage.connectionsUsed.toString())}`)
}
}
/**
* Display all active licenses
*/
private async displayAllLicenses(): Promise<void> {
if (this.activeLicenses.size === 0) {
console.log(boxen(
`${this.emojis.lock} ${this.colors.brain('NO ACTIVE LICENSES')} ${this.emojis.atom}\n\n` +
`${this.colors.dim('Start your atomic transformation with premium features:')}\n` +
`${this.colors.accent('→')} Run 'cortex license catalog' to browse features\n` +
`${this.colors.accent('→')} Run 'cortex license trial <feature>' to start free trial`,
{ padding: 1, borderStyle: 'round', borderColor: '#D67441' }
))
return
}
console.log(boxen(
`${this.emojis.premium} ${this.colors.brain('ACTIVE LICENSES')} ${this.emojis.atom}`,
{ padding: 1, borderStyle: 'round', borderColor: '#FFD700' }
))
for (const license of this.activeLicenses.values()) {
const feature = this.premiumFeatures.get(license.product)
const expiryDate = new Date(license.expiresAt)
const daysLeft = Math.ceil((expiryDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24))
const statusIcon = license.status === 'active' ? this.emojis.check :
license.status === 'trial' ? this.emojis.trial :
license.status === 'expired' ? this.emojis.cross : this.emojis.warning
console.log(
`\n ${statusIcon} ${this.colors.highlight(feature?.name || license.product)}\n` +
` ${this.colors.dim('License:')} ${this.colors.primary(license.id)}\n` +
` ${this.colors.dim('Type:')} ${this.colors.premium(license.type)} | ` +
`${this.colors.dim('Status:')} ${this.colors.success(license.status)} | ` +
`${this.colors.dim('Expires:')} ${daysLeft > 0 ? this.colors.info(`${daysLeft} days`) : this.colors.error('Expired')}`
)
}
console.log(`\n${this.colors.dim('Run')} ${this.colors.accent('cortex license status <license-id>')} ${this.colors.dim('for detailed information')}`)
}
}

View file

@ -7,8 +7,8 @@
import { BrainyData } from '../brainyData.js'
import { NounType, VerbType } from '../types/graphTypes.js'
import * as fs from 'fs/promises'
import * as path from 'path'
import * as fs from '../universal/fs.js'
import * as path from '../universal/path.js'
// @ts-ignore
import chalk from 'chalk'
// @ts-ignore

View file

@ -6,8 +6,8 @@
import { BrainyData } from '../brainyData.js'
import { Cortex } from './cortex.js'
import * as fs from 'fs/promises'
import * as path from 'path'
import * as fs from '../universal/fs.js'
import * as path from '../universal/path.js'
export interface ServiceConfig {
name: string

View file

@ -1,384 +0,0 @@
/**
* Webhook Manager for Cortex CLI
*
* 🧠 Manage webhooks through Cortex for enterprise integrations
*/
import chalk from 'chalk'
import boxen from 'boxen'
import Table from 'cli-table3'
import prompts from 'prompts'
import { WebhookSystem, WebhookBuilder, WebhookEventType } from '../webhooks/webhookSystem.js'
export class WebhookManager {
private webhookSystem: WebhookSystem
private colors: any
private emojis: any
constructor(brainy: any) {
this.webhookSystem = new WebhookSystem(brainy)
this.colors = {
primary: chalk.hex('#3A5F4A'),
success: chalk.hex('#2D4A3A'),
warning: chalk.hex('#D67441'),
error: chalk.hex('#B85C35'),
info: chalk.hex('#4A6B5A'),
dim: chalk.hex('#8A9B8A'),
highlight: chalk.hex('#E88B5A'),
accent: chalk.hex('#F5E6D3')
}
this.emojis = {
webhook: '🔔',
atom: '⚛️',
check: '✅',
cross: '❌',
warning: '⚠️',
sync: '🔄',
sparkle: '✨',
gear: '⚙️'
}
}
/**
* Add a new webhook interactively
*/
async addWebhook(): Promise<void> {
console.log(boxen(
`${this.emojis.webhook} ${this.colors.primary('WEBHOOK CONFIGURATION')} ${this.emojis.atom}`,
{ padding: 1, borderStyle: 'round', borderColor: '#E88B5A' }
))
const answers = await prompts([
{
type: 'text',
name: 'id',
message: 'Webhook ID (unique identifier):',
validate: (value: string) => value.length > 0 || 'ID is required'
},
{
type: 'text',
name: 'url',
message: 'Webhook URL:',
validate: (value: string) => {
try {
new URL(value)
return true
} catch {
return 'Invalid URL format'
}
}
},
{
type: 'multiselect',
name: 'events',
message: 'Select events to subscribe to:',
choices: [
{ title: 'Data Added', value: 'data.added', selected: true },
{ title: 'Data Updated', value: 'data.updated' },
{ title: 'Data Deleted', value: 'data.deleted' },
{ title: 'Augmentation Triggered', value: 'augmentation.triggered' },
{ title: 'Augmentation Completed', value: 'augmentation.completed', selected: true },
{ title: 'Augmentation Failed', value: 'augmentation.failed' },
{ title: 'Connector Sync Started', value: 'connector.sync.started' },
{ title: 'Connector Sync Completed', value: 'connector.sync.completed' },
{ title: 'Connector Sync Failed', value: 'connector.sync.failed' },
{ title: 'Graph Relationship Created', value: 'graph.relationship.created' },
{ title: 'System Alert', value: 'system.alert' }
],
min: 1
},
{
type: 'text',
name: 'secret',
message: 'Webhook secret (optional, for signature verification):'
},
{
type: 'confirm',
name: 'addHeaders',
message: 'Add custom headers?',
initial: false
}
])
if (!answers.id || !answers.url) {
console.log(this.colors.dim('Webhook configuration cancelled'))
return
}
const builder = new WebhookBuilder()
.url(answers.url)
.events(...answers.events as WebhookEventType[])
if (answers.secret) {
builder.secret(answers.secret)
}
// Add custom headers if requested
if (answers.addHeaders) {
const headers: Record<string, string> = {}
let addMore = true
while (addMore) {
const header = await prompts([
{
type: 'text',
name: 'key',
message: 'Header name:'
},
{
type: 'text',
name: 'value',
message: 'Header value:'
},
{
type: 'confirm',
name: 'continue',
message: 'Add another header?',
initial: false
}
])
if (header.key && header.value) {
headers[header.key] = header.value
}
addMore = header.continue
}
if (Object.keys(headers).length > 0) {
builder.headers(headers)
}
}
// Configure retry policy
const retryConfig = await prompts([
{
type: 'number',
name: 'maxRetries',
message: 'Max retry attempts:',
initial: 3,
min: 0,
max: 10
},
{
type: 'number',
name: 'backoffMs',
message: 'Initial retry delay (ms):',
initial: 1000,
min: 100,
max: 60000
}
])
builder.retry(retryConfig.maxRetries, retryConfig.backoffMs)
try {
await this.webhookSystem.registerWebhook(answers.id, builder.build())
console.log(boxen(
`${this.emojis.check} ${this.colors.success('WEBHOOK REGISTERED')} ${this.emojis.atom}\n\n` +
`${this.colors.accent('ID:')} ${answers.id}\n` +
`${this.colors.accent('URL:')} ${answers.url}\n` +
`${this.colors.accent('Events:')} ${answers.events.length} subscribed`,
{ padding: 1, borderStyle: 'round', borderColor: '#2D4A3A' }
))
// Offer to test
const { test } = await prompts({
type: 'confirm',
name: 'test',
message: 'Test webhook now?',
initial: true
})
if (test) {
await this.testWebhook(answers.id)
}
} catch (error: any) {
console.error(`${this.emojis.cross} Failed to register webhook:`, error.message)
}
}
/**
* List all webhooks
*/
async listWebhooks(): Promise<void> {
const webhooks = this.webhookSystem.listWebhooks()
const stats = this.webhookSystem.getStatistics()
console.log(boxen(
`${this.emojis.webhook} ${this.colors.primary('REGISTERED WEBHOOKS')} ${this.emojis.atom}\n\n` +
`${this.colors.accent('Total:')} ${stats.total}\n` +
`${this.colors.accent('Enabled:')} ${stats.enabled}\n` +
`${this.colors.accent('Failed Queues:')} ${stats.failedQueues.filter(q => q.count > 0).length}`,
{ padding: 1, borderStyle: 'round', borderColor: '#E88B5A' }
))
if (webhooks.length === 0) {
console.log(this.colors.dim('\nNo webhooks registered'))
console.log(this.colors.dim('Use "cortex webhook add" to register a webhook'))
return
}
const table = new Table({
head: ['ID', 'URL', 'Events', 'Status', 'Failed'],
style: {
head: ['cyan'],
border: ['grey']
}
})
for (const { id, config } of webhooks) {
const failedCount = stats.failedQueues.find(q => q.id === id)?.count || 0
table.push([
id,
config.url.length > 40 ? config.url.substring(0, 37) + '...' : config.url,
config.events.length.toString(),
config.enabled ? this.colors.success('Enabled') : this.colors.dim('Disabled'),
failedCount > 0 ? this.colors.warning(failedCount.toString()) : '-'
])
}
console.log(table.toString())
}
/**
* Test a webhook
*/
async testWebhook(id: string): Promise<void> {
const spinner = '⚛️'
console.log(`${spinner} Testing webhook ${id}...`)
try {
const success = await this.webhookSystem.testWebhook(id)
if (success) {
console.log(`${this.emojis.check} Webhook test successful! Server responded correctly.`)
} else {
console.log(`${this.emojis.cross} Webhook test failed. Check the URL and server.`)
}
} catch (error: any) {
console.error(`${this.emojis.cross} Test failed:`, error.message)
}
}
/**
* Remove a webhook
*/
async removeWebhook(id: string): Promise<void> {
const { confirm } = await prompts({
type: 'confirm',
name: 'confirm',
message: `Remove webhook "${id}"?`,
initial: false
})
if (!confirm) {
console.log(this.colors.dim('Cancelled'))
return
}
await this.webhookSystem.unregisterWebhook(id)
console.log(`${this.emojis.check} Webhook "${id}" removed`)
}
/**
* Retry failed webhooks
*/
async retryFailed(id: string): Promise<void> {
console.log(`${this.emojis.sync} Retrying failed webhooks for "${id}"...`)
const count = await this.webhookSystem.retryFailed(id)
if (count > 0) {
console.log(`${this.emojis.check} Retried ${count} failed webhook(s)`)
} else {
console.log(this.colors.dim('No failed webhooks to retry'))
}
}
/**
* Configure webhook interactively
*/
async configureWebhook(id: string): Promise<void> {
const webhooks = this.webhookSystem.listWebhooks()
const webhook = webhooks.find(w => w.id === id)
if (!webhook) {
console.error(`${this.emojis.cross} Webhook "${id}" not found`)
return
}
console.log(boxen(
`${this.emojis.gear} ${this.colors.primary('CONFIGURE WEBHOOK')} ${this.emojis.atom}\n\n` +
`${this.colors.accent('ID:')} ${id}\n` +
`${this.colors.accent('Current URL:')} ${webhook.config.url}`,
{ padding: 1, borderStyle: 'round', borderColor: '#E88B5A' }
))
const { action } = await prompts({
type: 'select',
name: 'action',
message: 'What would you like to configure?',
choices: [
{ title: 'Change URL', value: 'url' },
{ title: 'Update Events', value: 'events' },
{ title: 'Set Secret', value: 'secret' },
{ title: 'Toggle Enable/Disable', value: 'toggle' },
{ title: 'Update Retry Policy', value: 'retry' },
{ title: 'Cancel', value: 'cancel' }
]
})
switch (action) {
case 'url':
const { newUrl } = await prompts({
type: 'text',
name: 'newUrl',
message: 'New webhook URL:',
initial: webhook.config.url
})
if (newUrl) {
webhook.config.url = newUrl
await this.webhookSystem.unregisterWebhook(id)
await this.webhookSystem.registerWebhook(id, webhook.config)
console.log(`${this.emojis.check} URL updated`)
}
break
case 'toggle':
webhook.config.enabled = !webhook.config.enabled
await this.webhookSystem.unregisterWebhook(id)
await this.webhookSystem.registerWebhook(id, webhook.config)
console.log(`${this.emojis.check} Webhook ${webhook.config.enabled ? 'enabled' : 'disabled'}`)
break
case 'cancel':
console.log(this.colors.dim('Configuration cancelled'))
break
}
}
/**
* Show webhook statistics
*/
async showStatistics(): Promise<void> {
const stats = this.webhookSystem.getStatistics()
console.log(boxen(
`${this.emojis.webhook} ${this.colors.primary('WEBHOOK STATISTICS')} ${this.emojis.atom}\n\n` +
`${this.colors.accent('Total Webhooks:')} ${stats.total}\n` +
`${this.colors.accent('Enabled:')} ${stats.enabled}\n` +
`${this.colors.accent('Disabled:')} ${stats.total - stats.enabled}\n\n` +
`${this.colors.highlight('Failed Queues:')}\n` +
stats.failedQueues
.filter(q => q.count > 0)
.map(q => ` ${this.colors.warning('•')} ${q.id}: ${q.count} failed`)
.join('\n'),
{ padding: 1, borderStyle: 'round', borderColor: '#E88B5A' }
))
}
}

View file

@ -3,7 +3,7 @@
* Manages shared configuration in S3 for distributed Brainy instances
*/
import { v4 as uuidv4 } from 'uuid'
import { v4 as uuidv4 } from '../universal/uuid.js'
import {
DistributedConfig,
SharedConfig,

View file

@ -54,8 +54,8 @@ import { BrainyChat, ChatOptions } from './chat/brainyChat.js'
export { BrainyChat }
export type { ChatOptions }
// Export Cortex CLI functionality
export { Cortex } from './cortex/cortex.js'
// Export Cortex CLI functionality - commented out for core MIT build
// export { Cortex } from './cortex/cortex.js'
// Export performance and optimization utilities
import {
@ -247,6 +247,7 @@ export {
}
export type { AugmentationRegistryLoaderOptions, AugmentationLoadResult }
// Export augmentation implementations
import {
MemoryStorageAugmentation,

View file

@ -6,7 +6,7 @@
* and getting relationships.
*/
import { v4 as uuidv4 } from 'uuid'
import { v4 as uuidv4 } from '../universal/uuid.js'
import { BrainyDataInterface } from '../types/brainyDataInterface.js'
import {
MCPRequest,

View file

@ -7,7 +7,7 @@
* for external model access.
*/
import { v4 as uuidv4 } from 'uuid'
import { v4 as uuidv4 } from '../universal/uuid.js'
import { BrainyDataInterface } from '../types/brainyDataInterface.js'
import {
MCPRequest,

View file

@ -5,7 +5,7 @@
* It provides methods for getting available tools and executing tools.
*/
import { v4 as uuidv4 } from 'uuid'
import { v4 as uuidv4 } from '../universal/uuid.js'
import {
MCPResponse,
MCPToolExecutionRequest,

228
src/universal/crypto.ts Normal file
View file

@ -0,0 +1,228 @@
/**
* Universal Crypto implementation
* Works in all environments: Browser, Node.js, Serverless
*/
import { isBrowser, isNode } from '../utils/environment.js'
let nodeCrypto: any = null
// Dynamic import for Node.js crypto (only in Node.js environment)
if (isNode()) {
try {
nodeCrypto = await import('crypto')
} catch {
// Ignore import errors in non-Node environments
}
}
/**
* Generate random bytes
*/
export function randomBytes(size: number): Uint8Array {
if (isBrowser() || typeof crypto !== 'undefined') {
// Use Web Crypto API (available in browsers and modern Node.js)
const array = new Uint8Array(size)
crypto.getRandomValues(array)
return array
} else if (nodeCrypto) {
// Use Node.js crypto as fallback
return new Uint8Array(nodeCrypto.randomBytes(size))
} else {
// Fallback for environments without crypto
const array = new Uint8Array(size)
for (let i = 0; i < size; i++) {
array[i] = Math.floor(Math.random() * 256)
}
return array
}
}
/**
* Generate random UUID
*/
export function randomUUID(): string {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID()
} else if (nodeCrypto && nodeCrypto.randomUUID) {
return nodeCrypto.randomUUID()
} else {
// Fallback UUID generation
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0
const v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}
}
/**
* Create hash (simplified interface)
*/
export function createHash(algorithm: string): {
update: (data: string | Uint8Array) => any
digest: (encoding: string) => string
} {
if (nodeCrypto && nodeCrypto.createHash) {
return nodeCrypto.createHash(algorithm)
} else {
// Simple fallback hash for browsers (not cryptographically secure)
let hash = 0
const hashObj = {
update: (data: string | Uint8Array) => {
const text = typeof data === 'string' ? data : new TextDecoder().decode(data)
for (let i = 0; i < text.length; i++) {
const char = text.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash // Convert to 32-bit integer
}
return hashObj
},
digest: (encoding: string) => {
return Math.abs(hash).toString(16)
}
}
return hashObj
}
}
/**
* Create HMAC
*/
export function createHmac(algorithm: string, key: string | Uint8Array): {
update: (data: string | Uint8Array) => any
digest: (encoding: string) => string
} {
if (nodeCrypto && nodeCrypto.createHmac) {
return nodeCrypto.createHmac(algorithm, key)
} else {
// Fallback HMAC implementation (simplified)
return createHash(algorithm)
}
}
/**
* PBKDF2 synchronous
*/
export function pbkdf2Sync(password: string | Uint8Array, salt: string | Uint8Array, iterations: number, keylen: number, digest: string): Uint8Array {
if (nodeCrypto && nodeCrypto.pbkdf2Sync) {
return new Uint8Array(nodeCrypto.pbkdf2Sync(password, salt, iterations, keylen, digest))
} else {
// Simplified fallback (not cryptographically secure)
const result = new Uint8Array(keylen)
const passwordStr = typeof password === 'string' ? password : new TextDecoder().decode(password)
const saltStr = typeof salt === 'string' ? salt : new TextDecoder().decode(salt)
let hash = 0
const combined = passwordStr + saltStr
for (let i = 0; i < combined.length; i++) {
hash = ((hash << 5) - hash) + combined.charCodeAt(i)
hash = hash & hash
}
for (let i = 0; i < keylen; i++) {
result[i] = (Math.abs(hash + i) % 256)
}
return result
}
}
/**
* Scrypt synchronous
*/
export function scryptSync(password: string | Uint8Array, salt: string | Uint8Array, keylen: number, options?: any): Uint8Array {
if (nodeCrypto && nodeCrypto.scryptSync) {
return new Uint8Array(nodeCrypto.scryptSync(password, salt, keylen, options))
} else {
// Fallback to pbkdf2Sync
return pbkdf2Sync(password, salt, 10000, keylen, 'sha256')
}
}
/**
* Create cipher
*/
export function createCipheriv(algorithm: string, key: Uint8Array, iv: Uint8Array): {
update: (data: string, inputEncoding?: string, outputEncoding?: string) => string
final: (outputEncoding?: string) => string
} {
if (nodeCrypto && nodeCrypto.createCipheriv) {
return nodeCrypto.createCipheriv(algorithm, key, iv)
} else {
// Fallback encryption (XOR-based, not secure)
let encrypted = ''
return {
update: (data: string, inputEncoding?: string, outputEncoding?: string) => {
for (let i = 0; i < data.length; i++) {
const char = data.charCodeAt(i)
const keyByte = key[i % key.length]
const ivByte = iv[i % iv.length]
encrypted += String.fromCharCode(char ^ keyByte ^ ivByte)
}
return outputEncoding === 'hex' ? Buffer.from(encrypted, 'binary').toString('hex') : encrypted
},
final: (outputEncoding?: string) => {
return outputEncoding === 'hex' ? '' : ''
}
}
}
}
/**
* Create decipher
*/
export function createDecipheriv(algorithm: string, key: Uint8Array, iv: Uint8Array): {
update: (data: string, inputEncoding?: string, outputEncoding?: string) => string
final: (outputEncoding?: string) => string
} {
if (nodeCrypto && nodeCrypto.createDecipheriv) {
return nodeCrypto.createDecipheriv(algorithm, key, iv)
} else {
// Fallback decryption (XOR-based, matches createCipheriv)
let decrypted = ''
return {
update: (data: string, inputEncoding?: string, outputEncoding?: string) => {
const input = inputEncoding === 'hex' ? Buffer.from(data, 'hex').toString('binary') : data
for (let i = 0; i < input.length; i++) {
const char = input.charCodeAt(i)
const keyByte = key[i % key.length]
const ivByte = iv[i % iv.length]
decrypted += String.fromCharCode(char ^ keyByte ^ ivByte)
}
return decrypted
},
final: (outputEncoding?: string) => {
return ''
}
}
}
}
/**
* Timing safe equal
*/
export function timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean {
if (nodeCrypto && nodeCrypto.timingSafeEqual) {
return nodeCrypto.timingSafeEqual(a, b)
} else {
// Fallback implementation
if (a.length !== b.length) return false
let result = 0
for (let i = 0; i < a.length; i++) {
result |= a[i] ^ b[i]
}
return result === 0
}
}
export default {
randomBytes,
randomUUID,
createHash,
createHmac,
pbkdf2Sync,
scryptSync,
createCipheriv,
createDecipheriv,
timingSafeEqual
}

199
src/universal/events.ts Normal file
View file

@ -0,0 +1,199 @@
/**
* Universal Events implementation
* Browser: Uses EventTarget API
* Node.js: Uses built-in events module
*/
import { isBrowser, isNode } from '../utils/environment.js'
let nodeEvents: any = null
// Dynamic import for Node.js events (only in Node.js environment)
if (isNode()) {
try {
nodeEvents = await import('events')
} catch {
// Ignore import errors in non-Node environments
}
}
/**
* Universal EventEmitter interface
*/
export interface UniversalEventEmitter {
on(event: string, listener: (...args: any[]) => void): this
off(event: string, listener: (...args: any[]) => void): this
emit(event: string, ...args: any[]): boolean
once(event: string, listener: (...args: any[]) => void): this
removeAllListeners(event?: string): this
listenerCount(event: string): number
}
/**
* Browser implementation using EventTarget
*/
class BrowserEventEmitter extends EventTarget implements UniversalEventEmitter {
private listeners = new Map<string, Set<(...args: any[]) => void>>()
on(event: string, listener: (...args: any[]) => void): this {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set())
}
this.listeners.get(event)!.add(listener)
const handler = (e: Event) => {
const customEvent = e as CustomEvent
listener(...(customEvent.detail || []))
}
// Store original listener reference for removal
;(listener as any).__handler = handler
this.addEventListener(event, handler)
return this
}
off(event: string, listener: (...args: any[]) => void): this {
const eventListeners = this.listeners.get(event)
if (eventListeners) {
eventListeners.delete(listener)
const handler = (listener as any).__handler
if (handler) {
this.removeEventListener(event, handler)
delete (listener as any).__handler
}
}
return this
}
emit(event: string, ...args: any[]): boolean {
const customEvent = new CustomEvent(event, { detail: args })
this.dispatchEvent(customEvent)
const eventListeners = this.listeners.get(event)
return eventListeners ? eventListeners.size > 0 : false
}
once(event: string, listener: (...args: any[]) => void): this {
const onceListener = (...args: any[]) => {
this.off(event, onceListener)
listener(...args)
}
return this.on(event, onceListener)
}
removeAllListeners(event?: string): this {
if (event) {
const eventListeners = this.listeners.get(event)
if (eventListeners) {
for (const listener of eventListeners) {
this.off(event, listener)
}
}
} else {
for (const [eventName] of this.listeners) {
this.removeAllListeners(eventName)
}
}
return this
}
listenerCount(event: string): number {
const eventListeners = this.listeners.get(event)
return eventListeners ? eventListeners.size : 0
}
}
/**
* Node.js implementation using events.EventEmitter
*/
class NodeEventEmitter implements UniversalEventEmitter {
private emitter: any
constructor() {
this.emitter = new nodeEvents.EventEmitter()
}
on(event: string, listener: (...args: any[]) => void): this {
this.emitter.on(event, listener)
return this
}
off(event: string, listener: (...args: any[]) => void): this {
this.emitter.off(event, listener)
return this
}
emit(event: string, ...args: any[]): boolean {
return this.emitter.emit(event, ...args)
}
once(event: string, listener: (...args: any[]) => void): this {
this.emitter.once(event, listener)
return this
}
removeAllListeners(event?: string): this {
this.emitter.removeAllListeners(event)
return this
}
listenerCount(event: string): number {
return this.emitter.listenerCount(event)
}
}
/**
* Universal EventEmitter class
*/
export class EventEmitter implements UniversalEventEmitter {
private emitter: UniversalEventEmitter
constructor() {
if (isBrowser()) {
this.emitter = new BrowserEventEmitter()
} else if (isNode() && nodeEvents) {
this.emitter = new NodeEventEmitter()
} else {
this.emitter = new BrowserEventEmitter()
}
}
on(event: string, listener: (...args: any[]) => void): this {
this.emitter.on(event, listener)
return this
}
off(event: string, listener: (...args: any[]) => void): this {
this.emitter.off(event, listener)
return this
}
emit(event: string, ...args: any[]): boolean {
return this.emitter.emit(event, ...args)
}
once(event: string, listener: (...args: any[]) => void): this {
this.emitter.once(event, listener)
return this
}
removeAllListeners(event?: string): this {
this.emitter.removeAllListeners(event)
return this
}
listenerCount(event: string): number {
return this.emitter.listenerCount(event)
}
}
// Named export for compatibility
export { EventEmitter as default }
// Re-export Node.js EventEmitter class if available
export const NodeEventEmitterClass = nodeEvents?.EventEmitter || null

327
src/universal/fs.ts Normal file
View file

@ -0,0 +1,327 @@
/**
* Universal File System implementation
* Browser: Uses OPFS (Origin Private File System)
* Node.js: Uses built-in fs/promises
* Serverless: Uses memory-based fallback
*/
import { isBrowser, isNode } from '../utils/environment.js'
let nodeFs: any = null
// Dynamic import for Node.js fs (only in Node.js environment)
if (isNode()) {
try {
nodeFs = await import('fs/promises')
} catch {
// Ignore import errors in non-Node environments
}
}
/**
* Universal file operations interface
*/
export interface UniversalFS {
readFile(path: string): Promise<string>
writeFile(path: string, data: string): Promise<void>
mkdir(path: string, options?: { recursive?: boolean }): Promise<void>
exists(path: string): Promise<boolean>
readdir(path: string): Promise<string[]>
unlink(path: string): Promise<void>
stat(path: string): Promise<{ isFile(): boolean, isDirectory(): boolean }>
access(path: string, mode?: number): Promise<void>
}
/**
* Browser implementation using OPFS
*/
class BrowserFS implements UniversalFS {
private async getRoot(): Promise<FileSystemDirectoryHandle> {
if ('storage' in navigator && 'getDirectory' in navigator.storage) {
return await (navigator.storage as any).getDirectory()
}
throw new Error('OPFS not supported in this browser')
}
private async getFileHandle(path: string, create = false): Promise<FileSystemFileHandle> {
const root = await this.getRoot()
const parts = path.split('/').filter(p => p)
let dir = root
for (let i = 0; i < parts.length - 1; i++) {
dir = await dir.getDirectoryHandle(parts[i], { create })
}
const fileName = parts[parts.length - 1]
return await dir.getFileHandle(fileName, { create })
}
private async getDirHandle(path: string, create = false): Promise<FileSystemDirectoryHandle> {
const root = await this.getRoot()
const parts = path.split('/').filter(p => p)
let dir = root
for (const part of parts) {
dir = await dir.getDirectoryHandle(part, { create })
}
return dir
}
async readFile(path: string): Promise<string> {
try {
const fileHandle = await this.getFileHandle(path)
const file = await fileHandle.getFile()
return await file.text()
} catch (error) {
throw new Error(`File not found: ${path}`)
}
}
async writeFile(path: string, data: string): Promise<void> {
const fileHandle = await this.getFileHandle(path, true)
const writable = await fileHandle.createWritable()
await writable.write(data)
await writable.close()
}
async mkdir(path: string, options = { recursive: true }): Promise<void> {
await this.getDirHandle(path, true)
}
async exists(path: string): Promise<boolean> {
try {
await this.getFileHandle(path)
return true
} catch {
try {
await this.getDirHandle(path)
return true
} catch {
return false
}
}
}
async readdir(path: string): Promise<string[]> {
const dir = await this.getDirHandle(path)
const entries: string[] = []
for await (const [name] of dir.entries()) {
entries.push(name)
}
return entries
}
async unlink(path: string): Promise<void> {
const parts = path.split('/').filter(p => p)
const fileName = parts.pop()!
const dirPath = parts.join('/')
if (dirPath) {
const dir = await this.getDirHandle(dirPath)
await dir.removeEntry(fileName)
} else {
const root = await this.getRoot()
await root.removeEntry(fileName)
}
}
async stat(path: string): Promise<{ isFile(): boolean, isDirectory(): boolean }> {
try {
await this.getFileHandle(path)
return { isFile: () => true, isDirectory: () => false }
} catch {
try {
await this.getDirHandle(path)
return { isFile: () => false, isDirectory: () => true }
} catch {
throw new Error(`Path not found: ${path}`)
}
}
}
async access(path: string, mode?: number): Promise<void> {
const exists = await this.exists(path)
if (!exists) {
throw new Error(`ENOENT: no such file or directory, access '${path}'`)
}
}
}
/**
* Node.js implementation using fs/promises
*/
class NodeFS implements UniversalFS {
async readFile(path: string): Promise<string> {
return await nodeFs.readFile(path, 'utf-8')
}
async writeFile(path: string, data: string): Promise<void> {
await nodeFs.writeFile(path, data, 'utf-8')
}
async mkdir(path: string, options = { recursive: true }): Promise<void> {
await nodeFs.mkdir(path, options)
}
async exists(path: string): Promise<boolean> {
try {
await nodeFs.access(path)
return true
} catch {
return false
}
}
async readdir(path: string): Promise<string[]> {
return await nodeFs.readdir(path)
}
async unlink(path: string): Promise<void> {
await nodeFs.unlink(path)
}
async stat(path: string): Promise<{ isFile(): boolean, isDirectory(): boolean }> {
const stats = await nodeFs.stat(path)
return {
isFile: () => stats.isFile(),
isDirectory: () => stats.isDirectory()
}
}
async access(path: string, mode?: number): Promise<void> {
await nodeFs.access(path, mode)
}
}
/**
* Memory-based fallback for serverless/edge environments
*/
class MemoryFS implements UniversalFS {
private files = new Map<string, string>()
private dirs = new Set<string>()
async readFile(path: string): Promise<string> {
const content = this.files.get(path)
if (content === undefined) {
throw new Error(`File not found: ${path}`)
}
return content
}
async writeFile(path: string, data: string): Promise<void> {
this.files.set(path, data)
// Ensure parent directories exist
const parts = path.split('/').slice(0, -1)
for (let i = 1; i <= parts.length; i++) {
this.dirs.add(parts.slice(0, i).join('/'))
}
}
async mkdir(path: string, options = { recursive: true }): Promise<void> {
this.dirs.add(path)
if (options.recursive) {
const parts = path.split('/')
for (let i = 1; i <= parts.length; i++) {
this.dirs.add(parts.slice(0, i).join('/'))
}
}
}
async exists(path: string): Promise<boolean> {
return this.files.has(path) || this.dirs.has(path)
}
async readdir(path: string): Promise<string[]> {
const entries = new Set<string>()
const pathPrefix = path + '/'
for (const filePath of this.files.keys()) {
if (filePath.startsWith(pathPrefix)) {
const relativePath = filePath.slice(pathPrefix.length)
const firstSegment = relativePath.split('/')[0]
entries.add(firstSegment)
}
}
for (const dirPath of this.dirs) {
if (dirPath.startsWith(pathPrefix)) {
const relativePath = dirPath.slice(pathPrefix.length)
const firstSegment = relativePath.split('/')[0]
if (firstSegment) entries.add(firstSegment)
}
}
return Array.from(entries)
}
async unlink(path: string): Promise<void> {
this.files.delete(path)
}
async stat(path: string): Promise<{ isFile(): boolean, isDirectory(): boolean }> {
const isFile = this.files.has(path)
const isDir = this.dirs.has(path)
if (!isFile && !isDir) {
throw new Error(`Path not found: ${path}`)
}
return {
isFile: () => isFile,
isDirectory: () => isDir
}
}
async access(path: string, mode?: number): Promise<void> {
const exists = await this.exists(path)
if (!exists) {
throw new Error(`ENOENT: no such file or directory, access '${path}'`)
}
}
}
// Create the appropriate filesystem implementation
let fsImpl: UniversalFS
if (isBrowser()) {
fsImpl = new BrowserFS()
} else if (isNode() && nodeFs) {
fsImpl = new NodeFS()
} else {
fsImpl = new MemoryFS()
}
// Export the filesystem operations
export const readFile = fsImpl.readFile.bind(fsImpl)
export const writeFile = fsImpl.writeFile.bind(fsImpl)
export const mkdir = fsImpl.mkdir.bind(fsImpl)
export const exists = fsImpl.exists.bind(fsImpl)
export const readdir = fsImpl.readdir.bind(fsImpl)
export const unlink = fsImpl.unlink.bind(fsImpl)
export const stat = fsImpl.stat.bind(fsImpl)
export const access = fsImpl.access.bind(fsImpl)
// Default export with promises namespace compatibility
export default {
readFile,
writeFile,
mkdir,
exists,
readdir,
unlink,
stat,
access
}
// Named export for fs/promises compatibility
export const promises = {
readFile,
writeFile,
mkdir,
exists,
readdir,
unlink,
stat,
access
}

28
src/universal/index.ts Normal file
View file

@ -0,0 +1,28 @@
/**
* Universal adapters for cross-environment compatibility
* Provides consistent APIs across Browser, Node.js, and Serverless environments
*/
// UUID adapter
export * from './uuid.js'
export { default as uuid } from './uuid.js'
// Crypto adapter
export * from './crypto.js'
export { default as crypto } from './crypto.js'
// File system adapter
export * from './fs.js'
export { default as fs } from './fs.js'
// Path adapter
export * from './path.js'
export { default as path } from './path.js'
// Events adapter
export * from './events.js'
export { default as events } from './events.js'
// Convenience re-exports for common patterns
export { v4 as uuidv4 } from './uuid.js'
export { EventEmitter } from './events.js'

187
src/universal/path.ts Normal file
View file

@ -0,0 +1,187 @@
/**
* Universal Path implementation
* Browser: Manual path operations
* Node.js: Uses built-in path module
*/
import { isNode } from '../utils/environment.js'
let nodePath: any = null
// Dynamic import for Node.js path (only in Node.js environment)
if (isNode()) {
try {
nodePath = await import('path')
} catch {
// Ignore import errors in non-Node environments
}
}
/**
* Universal path operations
*/
export function join(...paths: string[]): string {
if (nodePath) {
return nodePath.join(...paths)
}
// Browser fallback implementation
const parts: string[] = []
for (const path of paths) {
if (path) {
parts.push(...path.split('/').filter(p => p))
}
}
return parts.join('/')
}
export function dirname(path: string): string {
if (nodePath) {
return nodePath.dirname(path)
}
// Browser fallback implementation
const parts = path.split('/').filter(p => p)
if (parts.length <= 1) return '.'
return parts.slice(0, -1).join('/')
}
export function basename(path: string, ext?: string): string {
if (nodePath) {
return nodePath.basename(path, ext)
}
// Browser fallback implementation
const parts = path.split('/')
let name = parts[parts.length - 1]
if (ext && name.endsWith(ext)) {
name = name.slice(0, -ext.length)
}
return name
}
export function extname(path: string): string {
if (nodePath) {
return nodePath.extname(path)
}
// Browser fallback implementation
const name = basename(path)
const lastDot = name.lastIndexOf('.')
return lastDot === -1 ? '' : name.slice(lastDot)
}
export function resolve(...paths: string[]): string {
if (nodePath) {
return nodePath.resolve(...paths)
}
// Browser fallback implementation
let resolved = ''
let resolvedAbsolute = false
for (let i = paths.length - 1; i >= -1 && !resolvedAbsolute; i--) {
const path = i >= 0 ? paths[i] : '/'
if (!path) continue
resolved = path + '/' + resolved
resolvedAbsolute = path.charAt(0) === '/'
}
// Normalize the path
resolved = normalizeArray(resolved.split('/').filter(p => p), !resolvedAbsolute).join('/')
return (resolvedAbsolute ? '/' : '') + resolved
}
export function relative(from: string, to: string): string {
if (nodePath) {
return nodePath.relative(from, to)
}
// Browser fallback implementation
const fromParts = resolve(from).split('/').filter(p => p)
const toParts = resolve(to).split('/').filter(p => p)
let commonLength = 0
for (let i = 0; i < Math.min(fromParts.length, toParts.length); i++) {
if (fromParts[i] === toParts[i]) {
commonLength++
} else {
break
}
}
const upCount = fromParts.length - commonLength
const upParts = new Array(upCount).fill('..')
const downParts = toParts.slice(commonLength)
return [...upParts, ...downParts].join('/')
}
export function isAbsolute(path: string): boolean {
if (nodePath) {
return nodePath.isAbsolute(path)
}
// Browser fallback implementation
return path.charAt(0) === '/'
}
/**
* Normalize array helper function
*/
function normalizeArray(parts: string[], allowAboveRoot: boolean): string[] {
const res: string[] = []
for (let i = 0; i < parts.length; i++) {
const p = parts[i]
if (!p || p === '.') continue
if (p === '..') {
if (res.length && res[res.length - 1] !== '..') {
res.pop()
} else if (allowAboveRoot) {
res.push('..')
}
} else {
res.push(p)
}
}
return res
}
// Path separator (always use forward slash for consistency)
export const sep = '/'
export const delimiter = ':'
// POSIX path object for compatibility
export const posix = {
join,
dirname,
basename,
extname,
resolve,
relative,
isAbsolute,
sep: '/',
delimiter: ':'
}
// Default export
export default {
join,
dirname,
basename,
extname,
resolve,
relative,
isAbsolute,
sep,
delimiter,
posix
}

26
src/universal/uuid.ts Normal file
View file

@ -0,0 +1,26 @@
/**
* Universal UUID implementation
* Works in all environments: Browser, Node.js, Serverless
*/
import { isBrowser, isNode } from '../utils/environment.js'
export function v4(): string {
// Use crypto.randomUUID if available (Node.js 19+, modern browsers)
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID()
}
// Fallback implementation for older environments
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0
const v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}
// Named export to match uuid package API
export { v4 as uuidv4 }
// Default export for convenience
export default { v4 }

View file

@ -1,428 +0,0 @@
/**
* Webhook System - Enterprise Event Notifications
*
* 🧠 Real-time notifications for augmentation events, data changes, and system alerts
* Critical for enterprise integrations and premium connectors
*/
import { EventEmitter } from 'events'
import { BrainyDataInterface } from '../types/brainyDataInterface.js'
export interface WebhookConfig {
url: string
events: WebhookEventType[]
headers?: Record<string, string>
secret?: string
retryPolicy?: {
maxRetries: number
backoffMs: number
maxBackoffMs: number
}
filters?: {
augmentations?: string[]
metadata?: Record<string, any>
}
enabled: boolean
}
export type WebhookEventType =
| 'data.added'
| 'data.updated'
| 'data.deleted'
| 'augmentation.triggered'
| 'augmentation.completed'
| 'augmentation.failed'
| 'connector.sync.started'
| 'connector.sync.completed'
| 'connector.sync.failed'
| 'graph.relationship.created'
| 'graph.relationship.deleted'
| 'system.alert'
| 'license.expired'
| 'license.renewed'
export interface WebhookPayload {
event: WebhookEventType
timestamp: string
data: any
metadata?: Record<string, any>
brainyId?: string
augmentationId?: string
signature?: string
}
export interface WebhookResponse {
success: boolean
statusCode?: number
error?: string
retryAfter?: number
}
export class WebhookSystem extends EventEmitter {
private webhooks: Map<string, WebhookConfig> = new Map()
private brainy: BrainyDataInterface
private retryQueues: Map<string, any[]> = new Map()
private isRunning: boolean = false
constructor(brainy: BrainyDataInterface) {
super()
this.brainy = brainy
this.setupEventListeners()
}
/**
* Register a new webhook
*/
async registerWebhook(id: string, config: WebhookConfig): Promise<void> {
// Validate URL
try {
new URL(config.url)
} catch {
throw new Error(`Invalid webhook URL: ${config.url}`)
}
// Set default retry policy
if (!config.retryPolicy) {
config.retryPolicy = {
maxRetries: 3,
backoffMs: 1000,
maxBackoffMs: 30000
}
}
this.webhooks.set(id, config)
this.retryQueues.set(id, [])
console.log(`🔔⚛️ Webhook registered: ${id}${config.url}`)
}
/**
* Remove a webhook
*/
async unregisterWebhook(id: string): Promise<void> {
this.webhooks.delete(id)
this.retryQueues.delete(id)
console.log(`🔔 Webhook unregistered: ${id}`)
}
/**
* List all webhooks
*/
listWebhooks(): Array<{ id: string; config: WebhookConfig }> {
return Array.from(this.webhooks.entries()).map(([id, config]) => ({
id,
config
}))
}
/**
* Trigger webhook for an event
*/
async triggerWebhook(event: WebhookEventType, data: any, metadata?: Record<string, any>): Promise<void> {
const payload: WebhookPayload = {
event,
timestamp: new Date().toISOString(),
data,
metadata
}
// Find webhooks subscribed to this event
for (const [id, config] of this.webhooks.entries()) {
if (!config.enabled) continue
if (!config.events.includes(event)) continue
// Apply filters
if (config.filters) {
if (config.filters.augmentations && metadata?.augmentation) {
if (!config.filters.augmentations.includes(metadata.augmentation)) {
continue
}
}
if (config.filters.metadata) {
let matchesFilter = true
for (const [key, value] of Object.entries(config.filters.metadata)) {
if (metadata?.[key] !== value) {
matchesFilter = false
break
}
}
if (!matchesFilter) continue
}
}
// Sign payload if secret provided
if (config.secret) {
payload.signature = await this.signPayload(payload, config.secret)
}
// Send webhook
this.sendWebhook(id, config, payload)
}
}
/**
* Send webhook with retry logic
*/
private async sendWebhook(
id: string,
config: WebhookConfig,
payload: WebhookPayload,
retryCount: number = 0
): Promise<void> {
try {
const response = await this.executeWebhook(config, payload)
if (response.success) {
console.log(`✅ Webhook delivered: ${id}${config.url}`)
this.emit('webhook:delivered', { id, payload, response })
} else {
throw new Error(response.error || `HTTP ${response.statusCode}`)
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error(`❌ Webhook failed: ${id}${errorMessage}`)
// Retry logic
if (retryCount < config.retryPolicy!.maxRetries) {
const backoff = Math.min(
config.retryPolicy!.backoffMs * Math.pow(2, retryCount),
config.retryPolicy!.maxBackoffMs
)
console.log(`🔄 Retrying webhook ${id} in ${backoff}ms (attempt ${retryCount + 1})`)
setTimeout(() => {
this.sendWebhook(id, config, payload, retryCount + 1)
}, backoff)
} else {
console.error(`❌ Webhook ${id} failed after ${retryCount} retries`)
this.emit('webhook:failed', { id, payload, error: errorMessage })
// Add to dead letter queue
this.retryQueues.get(id)?.push({ payload, failedAt: new Date() })
}
}
}
/**
* Execute HTTP webhook call
*/
private async executeWebhook(config: WebhookConfig, payload: WebhookPayload): Promise<WebhookResponse> {
try {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 10000) // 10 second timeout
const response = await fetch(config.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Brainy-Event': payload.event,
'X-Brainy-Signature': payload.signature || '',
...config.headers
},
body: JSON.stringify(payload),
signal: controller.signal
})
clearTimeout(timeout)
return {
success: response.ok,
statusCode: response.status,
error: response.ok ? undefined : `HTTP ${response.status}`
}
} catch (error: any) {
return {
success: false,
error: error.message
}
}
}
/**
* Sign webhook payload for security
*/
private async signPayload(payload: WebhookPayload, secret: string): Promise<string> {
const encoder = new TextEncoder()
const data = encoder.encode(JSON.stringify(payload))
const key = encoder.encode(secret)
// Simple HMAC-like signature (in production, use proper crypto)
const signature = btoa(String.fromCharCode(...new Uint8Array(data)))
return signature
}
/**
* Setup event listeners on Brainy
*/
private setupEventListeners(): void {
// Data events
this.brainy.on?.('data:added', (data) => {
this.triggerWebhook('data.added', data)
})
this.brainy.on?.('data:updated', (data) => {
this.triggerWebhook('data.updated', data)
})
this.brainy.on?.('data:deleted', (data) => {
this.triggerWebhook('data.deleted', data)
})
// Augmentation events
this.brainy.on?.('augmentation:triggered', (data) => {
this.triggerWebhook('augmentation.triggered', data, {
augmentation: data.augmentationId
})
})
this.brainy.on?.('augmentation:completed', (data) => {
this.triggerWebhook('augmentation.completed', data, {
augmentation: data.augmentationId
})
})
this.brainy.on?.('augmentation:failed', (data) => {
this.triggerWebhook('augmentation.failed', data, {
augmentation: data.augmentationId
})
})
// Graph events
this.brainy.on?.('graph:relationship:created', (data) => {
this.triggerWebhook('graph.relationship.created', data)
})
this.brainy.on?.('graph:relationship:deleted', (data) => {
this.triggerWebhook('graph.relationship.deleted', data)
})
}
/**
* Test webhook configuration
*/
async testWebhook(id: string): Promise<boolean> {
const config = this.webhooks.get(id)
if (!config) {
throw new Error(`Webhook ${id} not found`)
}
const testPayload: WebhookPayload = {
event: 'system.alert',
timestamp: new Date().toISOString(),
data: {
message: 'Test webhook from Brainy',
test: true
},
metadata: {
webhookId: id
}
}
if (config.secret) {
testPayload.signature = await this.signPayload(testPayload, config.secret)
}
const response = await this.executeWebhook(config, testPayload)
return response.success
}
/**
* Retry failed webhooks
*/
async retryFailed(id: string): Promise<number> {
const queue = this.retryQueues.get(id)
const config = this.webhooks.get(id)
if (!queue || !config) {
return 0
}
const failed = [...queue]
this.retryQueues.set(id, [])
let retried = 0
for (const item of failed) {
await this.sendWebhook(id, config, item.payload)
retried++
}
return retried
}
/**
* Get webhook statistics
*/
getStatistics(): {
total: number
enabled: number
failedQueues: Array<{ id: string; count: number }>
} {
const enabled = Array.from(this.webhooks.values()).filter(w => w.enabled).length
const failedQueues = Array.from(this.retryQueues.entries()).map(([id, queue]) => ({
id,
count: queue.length
}))
return {
total: this.webhooks.size,
enabled,
failedQueues
}
}
}
/**
* Webhook builder for easy configuration
*/
export class WebhookBuilder {
private config: Partial<WebhookConfig> = {
events: [],
enabled: true
}
url(url: string): this {
this.config.url = url
return this
}
events(...events: WebhookEventType[]): this {
this.config.events = events
return this
}
headers(headers: Record<string, string>): this {
this.config.headers = headers
return this
}
secret(secret: string): this {
this.config.secret = secret
return this
}
retry(maxRetries: number, backoffMs: number = 1000): this {
this.config.retryPolicy = {
maxRetries,
backoffMs,
maxBackoffMs: backoffMs * 10
}
return this
}
filter(filters: WebhookConfig['filters']): this {
this.config.filters = filters
return this
}
build(): WebhookConfig {
if (!this.config.url) {
throw new Error('Webhook URL is required')
}
if (!this.config.events || this.config.events.length === 0) {
throw new Error('At least one event type is required')
}
return this.config as WebhookConfig
}
}