Initial commit: Onebox v1.0.0

- Complete Deno-based architecture following nupst/spark patterns
- SQLite database with full schema
- Docker container management
- Service orchestration (Docker + Nginx + DNS + SSL)
- Registry authentication
- Nginx reverse proxy configuration
- Cloudflare DNS integration
- Let's Encrypt SSL automation
- Background daemon with metrics collection
- HTTP API server
- Comprehensive CLI
- Cross-platform compilation setup
- NPM distribution wrapper
- Shell installer script

Core features:
- Deploy containers with single command
- Automatic domain configuration
- Automatic SSL certificates
- Multi-registry support
- Metrics and logging
- Systemd integration

Ready for Angular UI implementation and testing.
This commit is contained in:
2025-10-28 13:05:42 +00:00
commit 246a6073e0
29 changed files with 5227 additions and 0 deletions

50
.gitignore vendored Normal file
View File

@@ -0,0 +1,50 @@
# Deno
.deno/
deno.lock
# Node modules (for npm wrapper)
node_modules/
# Build outputs
dist/binaries/*
!dist/binaries/.gitkeep
# Angular UI
ui/dist/
ui/node_modules/
ui/.angular/
# Development
.nogit/
*.log
*.db
*.db-journal
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Testing
coverage/
# SSL certificates (sensitive)
certs/
*.pem
*.key
*.crt
# Config with secrets
config.local.json
.env
.env.local
# Logs
logs/
*.log

70
bin/onebox-wrapper.js Normal file
View File

@@ -0,0 +1,70 @@
#!/usr/bin/env node
/**
* NPM wrapper for Onebox binary
*/
import { spawn } from 'child_process';
import { platform, arch } from 'os';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Detect platform and architecture
const platformMap = {
'linux': 'linux',
'darwin': 'macos',
'win32': 'windows',
};
const archMap = {
'x64': 'x64',
'arm64': 'arm64',
};
const currentPlatform = platformMap[platform()];
const currentArch = archMap[arch()];
if (!currentPlatform || !currentArch) {
console.error(`Unsupported platform: ${platform()} ${arch()}`);
process.exit(1);
}
// Build binary name
const binaryName = `onebox-${currentPlatform}-${currentArch}${currentPlatform === 'windows' ? '.exe' : ''}`;
const binaryPath = join(__dirname, '..', 'dist', 'binaries', binaryName);
// Spawn the binary
const child = spawn(binaryPath, process.argv.slice(2), {
stdio: 'inherit',
windowsHide: true,
});
// Forward signals
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
signals.forEach(signal => {
process.on(signal, () => {
if (!child.killed) {
child.kill(signal);
}
});
});
// Forward exit code
child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
} else {
process.exit(code || 0);
}
});
child.on('error', (error) => {
console.error(`Failed to start onebox: ${error.message}`);
console.error(`Binary path: ${binaryPath}`);
console.error('Make sure the binary was downloaded correctly.');
console.error('Try running: npm install -g @serve.zone/onebox');
process.exit(1);
});

29
changelog.md Normal file
View File

@@ -0,0 +1,29 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Initial project structure
- Core architecture classes
- Docker container management
- Nginx reverse proxy integration
- Cloudflare DNS management
- Let's Encrypt SSL automation
- SQLite database layer
- Angular web UI
- Multi-user authentication
- Systemd daemon integration
- CLI commands for all operations
- Metrics collection and historical data
- Log aggregation
- Registry authentication support
## [1.0.0] - TBD
### Added
- First stable release

44
deno.json Normal file
View File

@@ -0,0 +1,44 @@
{
"name": "@serve.zone/onebox",
"version": "1.0.0",
"exports": "./mod.ts",
"tasks": {
"test": "deno test --allow-all test/",
"test:watch": "deno test --allow-all --watch test/",
"compile": "bash scripts/compile-all.sh",
"dev": "deno run --allow-all --watch mod.ts"
},
"imports": {
"@std/path": "jsr:@std/path@^1.0.0",
"@std/fs": "jsr:@std/fs@^1.0.0",
"@std/http": "jsr:@std/http@^1.0.0",
"@std/assert": "jsr:@std/assert@^1.0.0",
"@std/encoding": "jsr:@std/encoding@^1.0.0",
"@db/sqlite": "jsr:@db/sqlite@^0.11.0",
"@push.rocks/smartdaemon": "npm:@push.rocks/smartdaemon@^2.0.0",
"@apiclient.xyz/docker": "npm:@apiclient.xyz/docker@^2.0.0",
"@apiclient.xyz/cloudflare": "npm:@apiclient.xyz/cloudflare@^2.0.0",
"@push.rocks/smartacme": "npm:@push.rocks/smartacme@^2.0.0"
},
"compilerOptions": {
"lib": ["deno.window", "deno.ns"],
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": false,
"noUnusedParameters": false
},
"fmt": {
"useTabs": false,
"lineWidth": 100,
"indentWidth": 2,
"semiColons": true,
"singleQuote": true,
"proseWrap": "preserve"
},
"lint": {
"rules": {
"tags": ["recommended"]
}
}
}

1
dist/binaries/.gitkeep vendored Normal file
View File

@@ -0,0 +1 @@
# Keep this directory in git

192
install.sh Executable file
View File

@@ -0,0 +1,192 @@
#!/bin/bash
#
# Onebox installer script
#
set -e
# Configuration
REPO_URL="https://code.foss.global/serve.zone/onebox"
INSTALL_DIR="/opt/onebox"
BIN_LINK="/usr/local/bin/onebox"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Functions
error() {
echo -e "${RED}Error: $1${NC}" >&2
exit 1
}
info() {
echo -e "${GREEN}$1${NC}"
}
warn() {
echo -e "${YELLOW}$1${NC}"
}
# Detect platform and architecture
detect_platform() {
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$OS" in
linux)
PLATFORM="linux"
;;
darwin)
PLATFORM="macos"
;;
*)
error "Unsupported operating system: $OS"
;;
esac
case "$ARCH" in
x86_64|amd64)
ARCH="x64"
;;
aarch64|arm64)
ARCH="arm64"
;;
*)
error "Unsupported architecture: $ARCH"
;;
esac
BINARY_NAME="onebox-${PLATFORM}-${ARCH}"
}
# Get latest version from Gitea API
get_latest_version() {
info "Fetching latest version..."
VERSION=$(curl -s "${REPO_URL}/releases" | grep -o '"tag_name":"v[^"]*' | head -1 | cut -d'"' -f4 | cut -c2-)
if [ -z "$VERSION" ]; then
warn "Could not fetch latest version, using 'main' branch"
VERSION="main"
else
info "Latest version: v${VERSION}"
fi
}
# Check if running as root
check_root() {
if [ "$EUID" -ne 0 ]; then
error "This script must be run as root (use sudo)"
fi
}
# Download binary
download_binary() {
info "Downloading Onebox ${VERSION} for ${PLATFORM}-${ARCH}..."
# Create temp directory
TMP_DIR=$(mktemp -d)
TMP_FILE="${TMP_DIR}/${BINARY_NAME}"
# Try release download first
if [ "$VERSION" != "main" ]; then
DOWNLOAD_URL="${REPO_URL}/releases/download/v${VERSION}/${BINARY_NAME}"
else
DOWNLOAD_URL="${REPO_URL}/raw/branch/main/dist/binaries/${BINARY_NAME}"
fi
if ! curl -L -f -o "$TMP_FILE" "$DOWNLOAD_URL"; then
error "Failed to download binary from $DOWNLOAD_URL"
fi
# Verify download
if [ ! -f "$TMP_FILE" ] || [ ! -s "$TMP_FILE" ]; then
error "Downloaded file is empty or missing"
fi
info "✓ Download complete"
}
# Install binary
install_binary() {
info "Installing Onebox to ${INSTALL_DIR}..."
# Create install directory
mkdir -p "$INSTALL_DIR"
# Copy binary
cp "$TMP_FILE" "${INSTALL_DIR}/onebox"
chmod +x "${INSTALL_DIR}/onebox"
# Create symlink
ln -sf "${INSTALL_DIR}/onebox" "$BIN_LINK"
# Cleanup temp files
rm -rf "$TMP_DIR"
info "✓ Installation complete"
}
# Initialize database and config
initialize() {
info "Initializing Onebox..."
# Create data directory
mkdir -p /var/lib/onebox
# Create certbot directory for ACME challenges
mkdir -p /var/www/certbot
info "✓ Initialization complete"
}
# Print success message
print_success() {
echo ""
info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
info " Onebox installed successfully!"
info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Next steps:"
echo ""
echo "1. Configure Cloudflare (optional):"
echo " onebox config set cloudflareAPIKey <key>"
echo " onebox config set cloudflareEmail <email>"
echo " onebox config set cloudflareZoneID <zone-id>"
echo " onebox config set serverIP <your-server-ip>"
echo ""
echo "2. Configure ACME email:"
echo " onebox config set acmeEmail <your@email.com>"
echo ""
echo "3. Install daemon:"
echo " onebox daemon install"
echo ""
echo "4. Start daemon:"
echo " onebox daemon start"
echo ""
echo "5. Deploy your first service:"
echo " onebox service add myapp --image nginx:latest --domain app.example.com"
echo ""
echo "Web UI: http://localhost:3000"
echo "Default credentials: admin / admin"
echo ""
}
# Main installation flow
main() {
info "Onebox Installer"
echo ""
check_root
detect_platform
get_latest_version
download_binary
install_binary
initialize
print_success
}
# Run main function
main

21
license Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Lossless GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

21
mod.ts Normal file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env -S deno run --allow-all
/**
* Onebox - Self-hosted container platform
*
* Entry point for the Onebox CLI and daemon.
*/
import { runCli } from './ts/index.ts';
if (import.meta.main) {
try {
await runCli();
} catch (error) {
console.error(`Error: ${error.message}`);
if (Deno.args.includes('--debug')) {
console.error(error.stack);
}
Deno.exit(1);
}
}

52
package.json Normal file
View File

@@ -0,0 +1,52 @@
{
"name": "@serve.zone/onebox",
"version": "1.0.0",
"description": "Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers",
"main": "mod.ts",
"type": "module",
"bin": {
"onebox": "./bin/onebox-wrapper.js"
},
"scripts": {
"postinstall": "node scripts/install-binary.js"
},
"keywords": [
"docker",
"containers",
"nginx",
"ssl",
"acme",
"letsencrypt",
"cloudflare",
"dns",
"heroku",
"paas",
"deployment"
],
"author": "Lossless GmbH",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://code.foss.global/serve.zone/onebox"
},
"homepage": "https://code.foss.global/serve.zone/onebox",
"bugs": {
"url": "https://code.foss.global/serve.zone/onebox/issues"
},
"files": [
"bin/",
"scripts/install-binary.js",
"readme.md",
"license",
"changelog.md"
],
"os": [
"linux",
"darwin",
"win32"
],
"cpu": [
"x64",
"arm64"
]
}

253
readme.hints.md Normal file
View File

@@ -0,0 +1,253 @@
# Onebox - Project Hints
## Architecture Overview
Onebox is a Deno-based self-hosted container platform that compiles to standalone binaries. It follows the same architectural patterns as nupst and spark projects.
### Core Components
1. **OneboxDatabase** (`ts/onebox.classes.database.ts`)
- SQLite-based storage
- Tables: services, registries, nginx_configs, ssl_certificates, dns_records, metrics, logs, users, settings
- Migration system for schema updates
2. **OneboxDockerManager** (`ts/onebox.classes.docker.ts`)
- Docker API integration via @apiclient.xyz/docker
- Container lifecycle management
- Network management (onebox-network bridge)
- Stats collection and logging
3. **OneboxServicesManager** (`ts/onebox.classes.services.ts`)
- High-level service orchestration
- Coordinates Docker + Nginx + DNS + SSL
- Service deployment workflow
4. **OneboxRegistriesManager** (`ts/onebox.classes.registries.ts`)
- Docker registry authentication
- Credential storage (encrypted)
- Auto-login on daemon start
5. **OneboxNginxManager** (`ts/onebox.classes.nginx.ts`)
- Nginx reverse proxy configuration
- Config file generation
- SSL enablement
- Reload and testing
6. **OneboxDnsManager** (`ts/onebox.classes.dns.ts`)
- Cloudflare API integration
- Automatic A record creation
- DNS sync and verification
7. **OneboxSslManager** (`ts/onebox.classes.ssl.ts`)
- Let's Encrypt integration via certbot
- Certificate issuance and renewal
- Expiry monitoring
8. **OneboxDaemon** (`ts/onebox.classes.daemon.ts`)
- Background monitoring loop
- Metrics collection (every 60s by default)
- SSL certificate renewal checks
- Service health monitoring
- Systemd integration
9. **OneboxHttpServer** (`ts/onebox.classes.httpserver.ts`)
- REST API endpoints
- Static file serving (for Angular UI)
- Authentication middleware
10. **Onebox** (`ts/onebox.classes.onebox.ts`)
- Main coordinator class
- Initializes all components
- Provides unified API
### CLI Structure
- `onebox service` - Service management
- `onebox registry` - Registry credentials
- `onebox dns` - DNS records
- `onebox ssl` - SSL certificates
- `onebox nginx` - Nginx control
- `onebox daemon` - Systemd daemon
- `onebox config` - Settings management
- `onebox status` - System status
### Deployment Workflow
1. User runs: `onebox service add myapp --image nginx --domain app.example.com`
2. Service record created in database
3. Docker image pulled from registry
4. Container created and started
5. Nginx config generated and reloaded
6. DNS record created (if configured)
7. SSL certificate obtained (if configured)
8. Service is live!
### Configuration
Settings stored in database (settings table):
- `cloudflareAPIKey` - Cloudflare API key
- `cloudflareEmail` - Cloudflare email
- `cloudflareZoneID` - Cloudflare zone ID
- `acmeEmail` - Let's Encrypt email
- `serverIP` - Server public IP
- `nginxConfigDir` - Custom nginx config directory
- `httpPort` - HTTP server port (default: 3000)
- `metricsInterval` - Metrics collection interval (default: 60000ms)
- `logRetentionDays` - Log retention period
### Data Locations
- Database: `/var/lib/onebox/onebox.db`
- Nginx configs: `/etc/nginx/sites-available/onebox-*`
- SSL certificates: `/etc/letsencrypt/live/<domain>/`
- Certbot webroot: `/var/www/certbot`
## Development
### Running Locally
```bash
# Development mode
deno task dev
# Run tests
deno task test
# Compile all binaries
deno task compile
```
### Adding a New Feature
1. Create new class in `ts/onebox.classes.<name>.ts`
2. Add to main Onebox class in `ts/onebox.classes.onebox.ts`
3. Add CLI commands in `ts/onebox.cli.ts`
4. Add API endpoints in `ts/onebox.classes.httpserver.ts`
5. Update types in `ts/onebox.types.ts`
6. Add tests in `test/`
7. Update documentation
### Database Migrations
Add migration logic in `OneboxDatabase.runMigrations()`:
```typescript
if (currentVersion === 1) {
this.db.query('ALTER TABLE services ADD COLUMN new_field TEXT');
this.setMigrationVersion(2);
}
```
## TODO
### Core Functionality (Complete ✓)
- [x] Database layer with SQLite
- [x] Docker integration
- [x] Service management
- [x] Registry authentication
- [x] Nginx reverse proxy
- [x] DNS management (Cloudflare)
- [x] SSL certificates (Let's Encrypt)
- [x] Background daemon
- [x] HTTP API server
- [x] CLI commands
- [x] Build system
### Next Steps
- [ ] Angular UI implementation
- Dashboard with service cards
- Service deployment form
- Logs viewer
- Metrics charts
- Settings page
- [ ] Authentication system (JWT)
- Login endpoint
- Token validation middleware
- Password hashing (bcrypt)
- [ ] WebSocket support for real-time logs/metrics
- [ ] Health checks for services
- [ ] Backup/restore functionality
- [ ] Multi-server support
- [ ] Load balancing
- [ ] Service templates/blueprints
### Testing
- [ ] Unit tests for all managers
- [ ] Integration tests for deployment workflow
- [ ] Mock Docker API for tests
- [ ] Database migration tests
### Documentation
- [ ] API documentation (OpenAPI/Swagger)
- [ ] Architecture diagram
- [ ] Deployment guide
- [ ] Troubleshooting guide
- [ ] Video tutorial
## Common Issues
### Docker Connection
If Docker commands fail, ensure:
- Docker daemon is running: `systemctl status docker`
- User has Docker permissions: `usermod -aG docker $USER`
- Socket exists: `ls -l /var/run/docker.sock`
### Nginx Issues
If nginx fails to reload:
- Check syntax: `onebox nginx test`
- Check logs: `journalctl -u nginx -n 50`
- Verify config files exist in `/etc/nginx/sites-available/`
### SSL Certificate Issues
If certbot fails:
- Verify domain DNS points to server
- Check port 80 is accessible
- Verify nginx is serving `.well-known/acme-challenge/`
- Check certbot logs: `journalctl -u certbot -n 50`
### Cloudflare DNS Issues
If DNS records aren't created:
- Verify API credentials: `onebox config show`
- Check zone ID matches your domain
- Verify API key has DNS edit permissions
## Dependencies
### Deno Packages
- `@std/path` - Path utilities
- `@std/fs` - Filesystem operations
- `@std/http` - HTTP server
- `@db/sqlite` - SQLite database
### NPM Packages (via Deno)
- `@push.rocks/smartdaemon` - Systemd integration
- `@apiclient.xyz/docker` - Docker API client
- `@apiclient.xyz/cloudflare` - Cloudflare API client
- `@push.rocks/smartacme` - ACME/Let's Encrypt
### System Dependencies
- `docker` - Container runtime
- `nginx` - Reverse proxy
- `certbot` - SSL certificates
- `systemd` - Service management
## Release Process
1. Update version in `deno.json`
2. Update `changelog.md`
3. Commit changes
4. Run `deno task compile` to build all binaries
5. Test binaries on each platform
6. Create git tag: `git tag v1.0.0`
7. Push tag: `git push origin v1.0.0`
8. Create Gitea release and upload binaries
9. Publish to npm: `pnpm publish`
## Notes
- Onebox requires root privileges for nginx, Docker, and port binding
- Default admin password should be changed immediately after installation
- Use `--debug` flag for verbose logging
- All Docker containers are on the `onebox-network` bridge
- Metrics are collected every 60 seconds by default
- SSL certificates auto-renew 30 days before expiry

209
readme.md Normal file
View File

@@ -0,0 +1,209 @@
# @serve.zone/onebox
> Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers
**Onebox** is a single-executable tool that transforms any Linux server into a simple container hosting platform. Deploy Docker containers with automatic HTTPS, DNS configuration, and Nginx reverse proxy - all managed through a beautiful Angular web interface or powerful CLI.
## Features
- 🐳 **Docker Container Management** - Deploy, start, stop, and manage containers
- 🌐 **Automatic Nginx Reverse Proxy** - Traffic routing with zero configuration
- 🔒 **Automatic SSL Certificates** - Let's Encrypt integration via SmartACME
- ☁️ **Cloudflare DNS Integration** - Automatic DNS record management
- 📊 **Metrics & Monitoring** - Historical CPU, memory, and network stats
- 📝 **Log Aggregation** - Centralized container logs
- 🎨 **Angular Web UI** - Modern, responsive interface
- 👥 **Multi-user Support** - Role-based access control
- 🔐 **Private Registry Support** - Use Docker Hub, Gitea, or custom registries
- 💾 **SQLite Database** - Embedded, zero-configuration storage
- 📦 **Single Executable** - No dependencies, no installation hassle
- 🔄 **Systemd Integration** - Run as a daemon with auto-restart
## Quick Start
### Installation
```bash
# Install via shell script
curl -sSL https://code.foss.global/serve.zone/onebox/raw/branch/main/install.sh | sudo bash
# Or via npm/pnpm
pnpm install -g @serve.zone/onebox
```
### Deploy Your First Service
```bash
# Add a registry (optional, for private images)
onebox registry add --url registry.example.com --username myuser --password mypass
# Deploy a service
onebox service add myapp \
--image nginx:latest \
--domain app.example.com \
--env PORT=80
# Check status
onebox service list
# View logs
onebox service logs myapp
```
### Install as Daemon
```bash
# Install systemd service
sudo onebox daemon install
# Start the daemon
sudo onebox daemon start
# View logs
sudo onebox daemon logs
```
### Access Web UI
The web UI is available at `http://localhost:3000` (or configured port).
Default credentials:
- Username: `admin`
- Password: `admin` (change immediately!)
## CLI Reference
### Service Management
```bash
onebox service add <name> --image <image> --domain <domain> [--env KEY=VALUE]
onebox service remove <name>
onebox service start <name>
onebox service stop <name>
onebox service restart <name>
onebox service list
onebox service logs <name> [--follow]
```
### Registry Management
```bash
onebox registry add --url <url> --username <user> --password <pass>
onebox registry remove <url>
onebox registry list
```
### DNS Management
```bash
onebox dns add <domain> --ip <ip>
onebox dns remove <domain>
onebox dns list
onebox dns sync
```
### SSL Management
```bash
onebox ssl renew [domain]
onebox ssl list
onebox ssl force-renew <domain>
```
### Nginx Management
```bash
onebox nginx reload
onebox nginx test
onebox nginx status
```
### Daemon Management
```bash
onebox daemon install
onebox daemon start
onebox daemon stop
onebox daemon restart
onebox daemon logs
```
### User Management
```bash
onebox user add <username> --password <password> [--role admin|user]
onebox user remove <username>
onebox user list
onebox user passwd <username>
```
### Configuration
```bash
onebox config show
onebox config set <key> <value>
```
### Metrics
```bash
onebox metrics [service-name]
```
## Architecture
Onebox is built with Deno and compiles to a standalone binary for each platform:
- **Deno Runtime** - Modern TypeScript with built-in security
- **SQLite** - Embedded database for configuration and metrics
- **Docker Engine** - Container runtime (required on host)
- **Nginx** - Reverse proxy and SSL termination
- **Cloudflare API** - DNS management
- **Let's Encrypt** - Free SSL certificates
- **Angular 18+** - Modern web interface
## Requirements
- **Linux** x64 or ARM64 (primary target)
- **Docker** installed and running
- **Nginx** installed
- **Root/sudo access** (for nginx, Docker, ports 80/443)
- **(Optional) Cloudflare account** for DNS management
## Development
```bash
# Clone repository
git clone https://code.foss.global/serve.zone/onebox
cd onebox
# Run in development mode
deno task dev
# Run tests
deno task test
# Compile for all platforms
deno task compile
```
## Configuration
Onebox stores configuration in:
- **Database**: `/var/lib/onebox/onebox.db`
- **Nginx configs**: `/etc/nginx/sites-available/onebox-*`
- **SSL certificates**: `/etc/letsencrypt/live/`
## Contributing
Contributions welcome! Please read the contributing guidelines first.
## License
MIT © Lossless GmbH
## Links
- [Documentation](https://code.foss.global/serve.zone/onebox/src/branch/main/docs)
- [Issue Tracker](https://code.foss.global/serve.zone/onebox/issues)
- [Changelog](./changelog.md)

56
scripts/compile-all.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/bin/bash
#
# Compile Onebox for all platforms
#
set -e
VERSION=$(grep '"version"' deno.json | cut -d'"' -f4)
echo "Compiling Onebox v${VERSION} for all platforms..."
# Create dist directory
mkdir -p dist/binaries
# Compile for each platform
echo "Compiling for Linux x64..."
deno compile --allow-all --no-check \
--output "dist/binaries/onebox-linux-x64" \
--target x86_64-unknown-linux-gnu \
mod.ts
echo "Compiling for Linux ARM64..."
deno compile --allow-all --no-check \
--output "dist/binaries/onebox-linux-arm64" \
--target aarch64-unknown-linux-gnu \
mod.ts
echo "Compiling for macOS x64..."
deno compile --allow-all --no-check \
--output "dist/binaries/onebox-macos-x64" \
--target x86_64-apple-darwin \
mod.ts
echo "Compiling for macOS ARM64..."
deno compile --allow-all --no-check \
--output "dist/binaries/onebox-macos-arm64" \
--target aarch64-apple-darwin \
mod.ts
echo "Compiling for Windows x64..."
deno compile --allow-all --no-check \
--output "dist/binaries/onebox-windows-x64.exe" \
--target x86_64-pc-windows-msvc \
mod.ts
echo ""
echo "✓ Compilation complete!"
echo ""
echo "Binaries:"
ls -lh dist/binaries/
echo ""
echo "Next steps:"
echo "1. Test binaries on their respective platforms"
echo "2. Create git tag: git tag v${VERSION}"
echo "3. Push tag: git push origin v${VERSION}"
echo "4. Upload binaries to Gitea release"
echo "5. Publish to npm: pnpm publish"

105
scripts/install-binary.js Normal file
View File

@@ -0,0 +1,105 @@
#!/usr/bin/env node
/**
* Postinstall script to download Onebox binary
*/
import { platform, arch } from 'os';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { createWriteStream, mkdirSync, chmodSync, existsSync } from 'fs';
import { get } from 'https';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Get version from package.json
import { readFileSync } from 'fs';
const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
const VERSION = packageJson.version;
// Detect platform
const platformMap = {
'linux': 'linux',
'darwin': 'macos',
'win32': 'windows',
};
const archMap = {
'x64': 'x64',
'arm64': 'arm64',
};
const currentPlatform = platformMap[platform()];
const currentArch = archMap[arch()];
if (!currentPlatform || !currentArch) {
console.error(`Unsupported platform: ${platform()} ${arch()}`);
process.exit(1);
}
const binaryName = `onebox-${currentPlatform}-${currentArch}${currentPlatform === 'windows' ? '.exe' : ''}`;
const binaryPath = join(__dirname, '..', 'dist', 'binaries', binaryName);
// Create directory
mkdirSync(join(__dirname, '..', 'dist', 'binaries'), { recursive: true });
// Check if binary already exists
if (existsSync(binaryPath)) {
console.log('Binary already exists, skipping download');
process.exit(0);
}
// Download URLs
const releaseUrl = `https://code.foss.global/serve.zone/onebox/releases/download/v${VERSION}/${binaryName}`;
const fallbackUrl = `https://code.foss.global/serve.zone/onebox/raw/branch/main/dist/binaries/${binaryName}`;
console.log(`Downloading Onebox v${VERSION} for ${currentPlatform}-${currentArch}...`);
function download(url, fallback = false) {
return new Promise((resolve, reject) => {
get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
// Follow redirect
download(response.headers.location, fallback).then(resolve).catch(reject);
return;
}
if (response.statusCode !== 200) {
if (!fallback) {
console.warn(`Release not found, trying fallback URL...`);
download(fallbackUrl, true).then(resolve).catch(reject);
} else {
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
}
return;
}
const fileStream = createWriteStream(binaryPath);
response.pipe(fileStream);
fileStream.on('finish', () => {
fileStream.close();
// Make executable (Unix only)
if (currentPlatform !== 'windows') {
chmodSync(binaryPath, 0o755);
}
console.log('✓ Binary downloaded successfully');
resolve();
});
fileStream.on('error', reject);
}).on('error', reject);
});
}
download(releaseUrl).catch((error) => {
console.error(`Failed to download binary: ${error.message}`);
console.error('You can manually download the binary from:');
console.error(releaseUrl);
console.error(`And place it at: ${binaryPath}`);
process.exit(1);
});

24
ts/index.ts Normal file
View File

@@ -0,0 +1,24 @@
/**
* Main exports and CLI entry point for Onebox
*/
export { Onebox } from './onebox.classes.onebox.ts';
export { runCli } from './onebox.cli.ts';
export { OneboxDatabase } from './onebox.classes.database.ts';
export { OneboxDockerManager } from './onebox.classes.docker.ts';
export { OneboxServicesManager } from './onebox.classes.services.ts';
export { OneboxRegistriesManager } from './onebox.classes.registries.ts';
export { OneboxNginxManager } from './onebox.classes.nginx.ts';
export { OneboxDnsManager } from './onebox.classes.dns.ts';
export { OneboxSslManager } from './onebox.classes.ssl.ts';
export { OneboxDaemon } from './onebox.classes.daemon.ts';
export { OneboxHttpServer } from './onebox.classes.httpserver.ts';
// Types
export * from './onebox.types.ts';
// Logging
export { logger } from './onebox.logging.ts';
// Version info
export { projectInfo } from './onebox.info.ts';

292
ts/onebox.classes.daemon.ts Normal file
View File

@@ -0,0 +1,292 @@
/**
* Daemon Manager for Onebox
*
* Handles background monitoring, metrics collection, and automatic tasks
*/
import * as plugins from './onebox.plugins.ts';
import { logger } from './onebox.logging.ts';
import { projectInfo } from './onebox.info.ts';
import type { Onebox } from './onebox.classes.onebox.ts';
export class OneboxDaemon {
private oneboxRef: Onebox;
private smartdaemon: plugins.smartdaemon.SmartDaemon;
private running = false;
private monitoringInterval: number | null = null;
private metricsInterval = 60000; // 1 minute
constructor(oneboxRef: Onebox) {
this.oneboxRef = oneboxRef;
this.smartdaemon = new plugins.smartdaemon.SmartDaemon();
// Get metrics interval from settings
const customInterval = this.oneboxRef.database.getSetting('metricsInterval');
if (customInterval) {
this.metricsInterval = parseInt(customInterval, 10);
}
}
/**
* Install systemd service
*/
async installService(): Promise<void> {
try {
logger.info('Installing Onebox daemon service...');
// Get installation directory
const execPath = Deno.execPath();
const service = await this.smartdaemon.addService({
name: 'onebox',
version: projectInfo.version,
command: `${execPath} run --allow-all ${Deno.cwd()}/mod.ts daemon start`,
description: 'Onebox - Self-hosted container platform',
workingDir: Deno.cwd(),
});
await service.save();
await service.enable();
logger.success('Onebox daemon service installed');
logger.info('Start with: sudo systemctl start smartdaemon_onebox');
} catch (error) {
logger.error(`Failed to install daemon service: ${error.message}`);
throw error;
}
}
/**
* Uninstall systemd service
*/
async uninstallService(): Promise<void> {
try {
logger.info('Uninstalling Onebox daemon service...');
const service = await this.smartdaemon.getService('onebox');
if (service) {
await service.stop();
await service.disable();
await service.delete();
}
logger.success('Onebox daemon service uninstalled');
} catch (error) {
logger.error(`Failed to uninstall daemon service: ${error.message}`);
throw error;
}
}
/**
* Start daemon mode (background monitoring)
*/
async start(): Promise<void> {
try {
if (this.running) {
logger.warn('Daemon already running');
return;
}
logger.info('Starting Onebox daemon...');
this.running = true;
// Start monitoring loop
this.startMonitoring();
// Start HTTP server
const httpPort = parseInt(this.oneboxRef.database.getSetting('httpPort') || '3000', 10);
await this.oneboxRef.httpServer.start(httpPort);
logger.success('Onebox daemon started');
logger.info(`Web UI available at http://localhost:${httpPort}`);
// Keep process alive
await this.keepAlive();
} catch (error) {
logger.error(`Failed to start daemon: ${error.message}`);
this.running = false;
throw error;
}
}
/**
* Stop daemon mode
*/
async stop(): Promise<void> {
try {
if (!this.running) {
return;
}
logger.info('Stopping Onebox daemon...');
this.running = false;
// Stop monitoring
this.stopMonitoring();
// Stop HTTP server
await this.oneboxRef.httpServer.stop();
logger.success('Onebox daemon stopped');
} catch (error) {
logger.error(`Failed to stop daemon: ${error.message}`);
throw error;
}
}
/**
* Start monitoring loop
*/
private startMonitoring(): void {
logger.info('Starting monitoring loop...');
this.monitoringInterval = setInterval(async () => {
await this.monitoringTick();
}, this.metricsInterval);
// Run first tick immediately
this.monitoringTick();
}
/**
* Stop monitoring loop
*/
private stopMonitoring(): void {
if (this.monitoringInterval !== null) {
clearInterval(this.monitoringInterval);
this.monitoringInterval = null;
logger.debug('Monitoring loop stopped');
}
}
/**
* Single monitoring tick
*/
private async monitoringTick(): Promise<void> {
try {
logger.debug('Running monitoring tick...');
// Collect metrics for all services
await this.collectMetrics();
// Sync service statuses
await this.oneboxRef.services.syncAllServiceStatuses();
// Check SSL certificate expiration
await this.checkSSLExpiration();
// Check service health (TODO: implement health checks)
logger.debug('Monitoring tick complete');
} catch (error) {
logger.error(`Monitoring tick failed: ${error.message}`);
}
}
/**
* Collect metrics for all services
*/
private async collectMetrics(): Promise<void> {
try {
const services = this.oneboxRef.services.listServices();
for (const service of services) {
if (service.status === 'running' && service.containerID) {
try {
const stats = await this.oneboxRef.docker.getContainerStats(service.containerID);
if (stats) {
this.oneboxRef.database.addMetric({
serviceId: service.id!,
timestamp: Date.now(),
cpuPercent: stats.cpuPercent,
memoryUsed: stats.memoryUsed,
memoryLimit: stats.memoryLimit,
networkRxBytes: stats.networkRx,
networkTxBytes: stats.networkTx,
});
}
} catch (error) {
logger.debug(`Failed to collect metrics for ${service.name}: ${error.message}`);
}
}
}
} catch (error) {
logger.error(`Failed to collect metrics: ${error.message}`);
}
}
/**
* Check SSL certificate expiration
*/
private async checkSSLExpiration(): Promise<void> {
try {
if (!this.oneboxRef.ssl.isConfigured()) {
return;
}
await this.oneboxRef.ssl.renewExpiring();
} catch (error) {
logger.error(`Failed to check SSL expiration: ${error.message}`);
}
}
/**
* Keep process alive
*/
private async keepAlive(): Promise<void> {
// Set up signal handlers
const signalHandler = () => {
logger.info('Received shutdown signal');
this.stop().then(() => {
Deno.exit(0);
});
};
Deno.addSignalListener('SIGINT', signalHandler);
Deno.addSignalListener('SIGTERM', signalHandler);
// Keep event loop alive
while (this.running) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
/**
* Get daemon status
*/
isRunning(): boolean {
return this.running;
}
/**
* Get service status from systemd
*/
async getServiceStatus(): Promise<string> {
try {
const command = new Deno.Command('systemctl', {
args: ['status', 'smartdaemon_onebox'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stdout } = await command.output();
const output = new TextDecoder().decode(stdout);
if (code === 0 || output.includes('active (running)')) {
return 'running';
} else if (output.includes('inactive') || output.includes('dead')) {
return 'stopped';
} else if (output.includes('failed')) {
return 'failed';
} else {
return 'unknown';
}
} catch (error) {
return 'not-installed';
}
}
}

View File

@@ -0,0 +1,659 @@
/**
* Database layer for Onebox using SQLite
*/
import * as plugins from './onebox.plugins.ts';
import type {
IService,
IRegistry,
INginxConfig,
ISslCertificate,
IDnsRecord,
IMetric,
ILogEntry,
IUser,
ISetting,
} from './onebox.types.ts';
import { logger } from './onebox.logging.ts';
export class OneboxDatabase {
private db: plugins.sqlite.DB | null = null;
private dbPath: string;
constructor(dbPath = '/var/lib/onebox/onebox.db') {
this.dbPath = dbPath;
}
/**
* Initialize database connection and create tables
*/
async init(): Promise<void> {
try {
// Ensure data directory exists
const dbDir = plugins.path.dirname(this.dbPath);
await Deno.mkdir(dbDir, { recursive: true });
// Open database
this.db = new plugins.sqlite.DB(this.dbPath);
logger.info(`Database initialized at ${this.dbPath}`);
// Create tables
await this.createTables();
// Run migrations if needed
await this.runMigrations();
} catch (error) {
logger.error(`Failed to initialize database: ${error.message}`);
throw error;
}
}
/**
* Create all database tables
*/
private async createTables(): Promise<void> {
if (!this.db) throw new Error('Database not initialized');
// Services table
this.db.query(`
CREATE TABLE IF NOT EXISTS services (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
image TEXT NOT NULL,
registry TEXT,
env_vars TEXT NOT NULL,
port INTEGER NOT NULL,
domain TEXT,
container_id TEXT,
status TEXT NOT NULL DEFAULT 'stopped',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Registries table
this.db.query(`
CREATE TABLE IF NOT EXISTS registries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL UNIQUE,
username TEXT NOT NULL,
password_encrypted TEXT NOT NULL,
created_at INTEGER NOT NULL
)
`);
// Nginx configs table
this.db.query(`
CREATE TABLE IF NOT EXISTS nginx_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
service_id INTEGER NOT NULL,
domain TEXT NOT NULL,
port INTEGER NOT NULL,
ssl_enabled INTEGER NOT NULL DEFAULT 0,
config_template TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
)
`);
// SSL certificates table
this.db.query(`
CREATE TABLE IF NOT EXISTS ssl_certificates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain TEXT NOT NULL UNIQUE,
cert_path TEXT NOT NULL,
key_path TEXT NOT NULL,
full_chain_path TEXT NOT NULL,
expiry_date INTEGER NOT NULL,
issuer TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// DNS records table
this.db.query(`
CREATE TABLE IF NOT EXISTS dns_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain TEXT NOT NULL UNIQUE,
type TEXT NOT NULL,
value TEXT NOT NULL,
cloudflare_id TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Metrics table
this.db.query(`
CREATE TABLE IF NOT EXISTS metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
service_id INTEGER NOT NULL,
timestamp INTEGER NOT NULL,
cpu_percent REAL NOT NULL,
memory_used INTEGER NOT NULL,
memory_limit INTEGER NOT NULL,
network_rx_bytes INTEGER NOT NULL,
network_tx_bytes INTEGER NOT NULL,
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
)
`);
// Create index for metrics queries
this.db.query(`
CREATE INDEX IF NOT EXISTS idx_metrics_service_timestamp
ON metrics(service_id, timestamp DESC)
`);
// Logs table
this.db.query(`
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
service_id INTEGER NOT NULL,
timestamp INTEGER NOT NULL,
message TEXT NOT NULL,
level TEXT NOT NULL,
source TEXT NOT NULL,
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
)
`);
// Create index for logs queries
this.db.query(`
CREATE INDEX IF NOT EXISTS idx_logs_service_timestamp
ON logs(service_id, timestamp DESC)
`);
// Users table
this.db.query(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Settings table
this.db.query(`
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Version table for migrations
this.db.query(`
CREATE TABLE IF NOT EXISTS migrations (
version INTEGER PRIMARY KEY,
applied_at INTEGER NOT NULL
)
`);
logger.debug('Database tables created successfully');
}
/**
* Run database migrations
*/
private async runMigrations(): Promise<void> {
if (!this.db) throw new Error('Database not initialized');
const currentVersion = this.getMigrationVersion();
logger.debug(`Current database version: ${currentVersion}`);
// Add migration logic here as needed
// For now, just set version to 1
if (currentVersion === 0) {
this.setMigrationVersion(1);
}
}
/**
* Get current migration version
*/
private getMigrationVersion(): number {
if (!this.db) throw new Error('Database not initialized');
try {
const result = this.db.query('SELECT MAX(version) as version FROM migrations');
return result.length > 0 && result[0][0] !== null ? Number(result[0][0]) : 0;
} catch {
return 0;
}
}
/**
* Set migration version
*/
private setMigrationVersion(version: number): void {
if (!this.db) throw new Error('Database not initialized');
this.db.query('INSERT INTO migrations (version, applied_at) VALUES (?, ?)', [
version,
Date.now(),
]);
logger.debug(`Migration version set to ${version}`);
}
/**
* Close database connection
*/
close(): void {
if (this.db) {
this.db.close();
this.db = null;
logger.debug('Database connection closed');
}
}
/**
* Execute a raw query
*/
query<T = unknown[]>(sql: string, params: unknown[] = []): T[] {
if (!this.db) throw new Error('Database not initialized');
return this.db.query(sql, params) as T[];
}
// ============ Services CRUD ============
async createService(service: Omit<IService, 'id'>): Promise<IService> {
if (!this.db) throw new Error('Database not initialized');
const now = Date.now();
this.db.query(
`INSERT INTO services (name, image, registry, env_vars, port, domain, container_id, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
service.name,
service.image,
service.registry || null,
JSON.stringify(service.envVars),
service.port,
service.domain || null,
service.containerID || null,
service.status,
now,
now,
]
);
return this.getServiceByName(service.name)!;
}
getServiceByName(name: string): IService | null {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM services WHERE name = ?', [name]);
return rows.length > 0 ? this.rowToService(rows[0]) : null;
}
getServiceByID(id: number): IService | null {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM services WHERE id = ?', [id]);
return rows.length > 0 ? this.rowToService(rows[0]) : null;
}
getAllServices(): IService[] {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM services ORDER BY created_at DESC');
return rows.map((row) => this.rowToService(row));
}
updateService(id: number, updates: Partial<IService>): void {
if (!this.db) throw new Error('Database not initialized');
const fields: string[] = [];
const values: unknown[] = [];
if (updates.image !== undefined) {
fields.push('image = ?');
values.push(updates.image);
}
if (updates.registry !== undefined) {
fields.push('registry = ?');
values.push(updates.registry);
}
if (updates.envVars !== undefined) {
fields.push('env_vars = ?');
values.push(JSON.stringify(updates.envVars));
}
if (updates.port !== undefined) {
fields.push('port = ?');
values.push(updates.port);
}
if (updates.domain !== undefined) {
fields.push('domain = ?');
values.push(updates.domain);
}
if (updates.containerID !== undefined) {
fields.push('container_id = ?');
values.push(updates.containerID);
}
if (updates.status !== undefined) {
fields.push('status = ?');
values.push(updates.status);
}
fields.push('updated_at = ?');
values.push(Date.now());
values.push(id);
this.db.query(`UPDATE services SET ${fields.join(', ')} WHERE id = ?`, values);
}
deleteService(id: number): void {
if (!this.db) throw new Error('Database not initialized');
this.db.query('DELETE FROM services WHERE id = ?', [id]);
}
private rowToService(row: unknown[]): IService {
return {
id: Number(row[0]),
name: String(row[1]),
image: String(row[2]),
registry: row[3] ? String(row[3]) : undefined,
envVars: JSON.parse(String(row[4])),
port: Number(row[5]),
domain: row[6] ? String(row[6]) : undefined,
containerID: row[7] ? String(row[7]) : undefined,
status: String(row[8]) as IService['status'],
createdAt: Number(row[9]),
updatedAt: Number(row[10]),
};
}
// ============ Registries CRUD ============
async createRegistry(registry: Omit<IRegistry, 'id'>): Promise<IRegistry> {
if (!this.db) throw new Error('Database not initialized');
const now = Date.now();
this.db.query(
'INSERT INTO registries (url, username, password_encrypted, created_at) VALUES (?, ?, ?, ?)',
[registry.url, registry.username, registry.passwordEncrypted, now]
);
return this.getRegistryByURL(registry.url)!;
}
getRegistryByURL(url: string): IRegistry | null {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM registries WHERE url = ?', [url]);
return rows.length > 0 ? this.rowToRegistry(rows[0]) : null;
}
getAllRegistries(): IRegistry[] {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM registries ORDER BY created_at DESC');
return rows.map((row) => this.rowToRegistry(row));
}
deleteRegistry(url: string): void {
if (!this.db) throw new Error('Database not initialized');
this.db.query('DELETE FROM registries WHERE url = ?', [url]);
}
private rowToRegistry(row: unknown[]): IRegistry {
return {
id: Number(row[0]),
url: String(row[1]),
username: String(row[2]),
passwordEncrypted: String(row[3]),
createdAt: Number(row[4]),
};
}
// ============ Settings CRUD ============
getSetting(key: string): string | null {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT value FROM settings WHERE key = ?', [key]);
return rows.length > 0 ? String(rows[0][0]) : null;
}
setSetting(key: string, value: string): void {
if (!this.db) throw new Error('Database not initialized');
const now = Date.now();
this.db.query(
'INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)',
[key, value, now]
);
}
getAllSettings(): Record<string, string> {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT key, value FROM settings');
const settings: Record<string, string> = {};
for (const row of rows) {
settings[String(row[0])] = String(row[1]);
}
return settings;
}
// ============ Users CRUD ============
async createUser(user: Omit<IUser, 'id'>): Promise<IUser> {
if (!this.db) throw new Error('Database not initialized');
const now = Date.now();
this.db.query(
'INSERT INTO users (username, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?)',
[user.username, user.passwordHash, user.role, now, now]
);
return this.getUserByUsername(user.username)!;
}
getUserByUsername(username: string): IUser | null {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM users WHERE username = ?', [username]);
return rows.length > 0 ? this.rowToUser(rows[0]) : null;
}
getAllUsers(): IUser[] {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM users ORDER BY created_at DESC');
return rows.map((row) => this.rowToUser(row));
}
updateUserPassword(username: string, passwordHash: string): void {
if (!this.db) throw new Error('Database not initialized');
this.db.query('UPDATE users SET password_hash = ?, updated_at = ? WHERE username = ?', [
passwordHash,
Date.now(),
username,
]);
}
deleteUser(username: string): void {
if (!this.db) throw new Error('Database not initialized');
this.db.query('DELETE FROM users WHERE username = ?', [username]);
}
private rowToUser(row: unknown[]): IUser {
return {
id: Number(row[0]),
username: String(row[1]),
passwordHash: String(row[2]),
role: String(row[3]) as IUser['role'],
createdAt: Number(row[4]),
updatedAt: Number(row[5]),
};
}
// ============ Metrics ============
addMetric(metric: Omit<IMetric, 'id'>): void {
if (!this.db) throw new Error('Database not initialized');
this.db.query(
`INSERT INTO metrics (service_id, timestamp, cpu_percent, memory_used, memory_limit, network_rx_bytes, network_tx_bytes)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
metric.serviceId,
metric.timestamp,
metric.cpuPercent,
metric.memoryUsed,
metric.memoryLimit,
metric.networkRxBytes,
metric.networkTxBytes,
]
);
}
getMetrics(serviceId: number, limit = 100): IMetric[] {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query(
'SELECT * FROM metrics WHERE service_id = ? ORDER BY timestamp DESC LIMIT ?',
[serviceId, limit]
);
return rows.map((row) => this.rowToMetric(row));
}
private rowToMetric(row: unknown[]): IMetric {
return {
id: Number(row[0]),
serviceId: Number(row[1]),
timestamp: Number(row[2]),
cpuPercent: Number(row[3]),
memoryUsed: Number(row[4]),
memoryLimit: Number(row[5]),
networkRxBytes: Number(row[6]),
networkTxBytes: Number(row[7]),
};
}
// ============ Logs ============
addLog(log: Omit<ILogEntry, 'id'>): void {
if (!this.db) throw new Error('Database not initialized');
this.db.query(
'INSERT INTO logs (service_id, timestamp, message, level, source) VALUES (?, ?, ?, ?, ?)',
[log.serviceId, log.timestamp, log.message, log.level, log.source]
);
}
getLogs(serviceId: number, limit = 1000): ILogEntry[] {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query(
'SELECT * FROM logs WHERE service_id = ? ORDER BY timestamp DESC LIMIT ?',
[serviceId, limit]
);
return rows.map((row) => this.rowToLog(row));
}
private rowToLog(row: unknown[]): ILogEntry {
return {
id: Number(row[0]),
serviceId: Number(row[1]),
timestamp: Number(row[2]),
message: String(row[3]),
level: String(row[4]) as ILogEntry['level'],
source: String(row[5]) as ILogEntry['source'],
};
}
// ============ SSL Certificates ============
async createSSLCertificate(cert: Omit<ISslCertificate, 'id'>): Promise<ISslCertificate> {
if (!this.db) throw new Error('Database not initialized');
const now = Date.now();
this.db.query(
`INSERT INTO ssl_certificates (domain, cert_path, key_path, full_chain_path, expiry_date, issuer, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
cert.domain,
cert.certPath,
cert.keyPath,
cert.fullChainPath,
cert.expiryDate,
cert.issuer,
now,
now,
]
);
return this.getSSLCertificate(cert.domain)!;
}
getSSLCertificate(domain: string): ISslCertificate | null {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM ssl_certificates WHERE domain = ?', [domain]);
return rows.length > 0 ? this.rowToSSLCert(rows[0]) : null;
}
getAllSSLCertificates(): ISslCertificate[] {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM ssl_certificates ORDER BY expiry_date ASC');
return rows.map((row) => this.rowToSSLCert(row));
}
updateSSLCertificate(domain: string, updates: Partial<ISslCertificate>): void {
if (!this.db) throw new Error('Database not initialized');
const fields: string[] = [];
const values: unknown[] = [];
if (updates.certPath) {
fields.push('cert_path = ?');
values.push(updates.certPath);
}
if (updates.keyPath) {
fields.push('key_path = ?');
values.push(updates.keyPath);
}
if (updates.fullChainPath) {
fields.push('full_chain_path = ?');
values.push(updates.fullChainPath);
}
if (updates.expiryDate) {
fields.push('expiry_date = ?');
values.push(updates.expiryDate);
}
fields.push('updated_at = ?');
values.push(Date.now());
values.push(domain);
this.db.query(`UPDATE ssl_certificates SET ${fields.join(', ')} WHERE domain = ?`, values);
}
deleteSSLCertificate(domain: string): void {
if (!this.db) throw new Error('Database not initialized');
this.db.query('DELETE FROM ssl_certificates WHERE domain = ?', [domain]);
}
private rowToSSLCert(row: unknown[]): ISslCertificate {
return {
id: Number(row[0]),
domain: String(row[1]),
certPath: String(row[2]),
keyPath: String(row[3]),
fullChainPath: String(row[4]),
expiryDate: Number(row[5]),
issuer: String(row[6]),
createdAt: Number(row[7]),
updatedAt: Number(row[8]),
};
}
}

270
ts/onebox.classes.dns.ts Normal file
View File

@@ -0,0 +1,270 @@
/**
* DNS Manager for Onebox
*
* Manages DNS records via Cloudflare API
*/
import * as plugins from './onebox.plugins.ts';
import { logger } from './onebox.logging.ts';
import { OneboxDatabase } from './onebox.classes.database.ts';
export class OneboxDnsManager {
private oneboxRef: any;
private database: OneboxDatabase;
private cloudflareClient: plugins.cloudflare.Cloudflare | null = null;
private zoneID: string | null = null;
private serverIP: string | null = null;
constructor(oneboxRef: any) {
this.oneboxRef = oneboxRef;
this.database = oneboxRef.database;
}
/**
* Initialize DNS manager with Cloudflare credentials
*/
async init(): Promise<void> {
try {
// Get Cloudflare credentials from settings
const apiKey = this.database.getSetting('cloudflareAPIKey');
const email = this.database.getSetting('cloudflareEmail');
const zoneID = this.database.getSetting('cloudflareZoneID');
const serverIP = this.database.getSetting('serverIP');
if (!apiKey || !email || !zoneID) {
logger.warn('Cloudflare credentials not configured. DNS management will be disabled.');
logger.info('Configure with: onebox config set cloudflareAPIKey <key>');
return;
}
this.zoneID = zoneID;
this.serverIP = serverIP;
// Initialize Cloudflare client
this.cloudflareClient = new plugins.cloudflare.Cloudflare({
apiKey,
email,
});
logger.info('DNS manager initialized with Cloudflare');
} catch (error) {
logger.error(`Failed to initialize DNS manager: ${error.message}`);
throw error;
}
}
/**
* Check if DNS manager is configured
*/
isConfigured(): boolean {
return this.cloudflareClient !== null && this.zoneID !== null;
}
/**
* Add a DNS record for a domain
*/
async addDNSRecord(domain: string, ip?: string): Promise<void> {
try {
if (!this.isConfigured()) {
throw new Error('DNS manager not configured');
}
logger.info(`Adding DNS record for ${domain}`);
const targetIP = ip || this.serverIP;
if (!targetIP) {
throw new Error('Server IP not configured. Set with: onebox config set serverIP <ip>');
}
// Check if record already exists
const existing = await this.getDNSRecord(domain);
if (existing) {
logger.info(`DNS record already exists for ${domain}`);
return;
}
// Create A record
const response = await this.cloudflareClient!.zones.dns.records.create(this.zoneID!, {
type: 'A',
name: domain,
content: targetIP,
ttl: 1, // Auto
proxied: false, // Don't proxy through Cloudflare for direct SSL
});
// Store in database
await this.database.query(
'INSERT INTO dns_records (domain, type, value, cloudflare_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[domain, 'A', targetIP, response.result.id, Date.now(), Date.now()]
);
logger.success(`DNS record created for ${domain}${targetIP}`);
} catch (error) {
logger.error(`Failed to add DNS record for ${domain}: ${error.message}`);
throw error;
}
}
/**
* Remove a DNS record
*/
async removeDNSRecord(domain: string): Promise<void> {
try {
if (!this.isConfigured()) {
throw new Error('DNS manager not configured');
}
logger.info(`Removing DNS record for ${domain}`);
// Get record from database
const rows = this.database.query('SELECT cloudflare_id FROM dns_records WHERE domain = ?', [
domain,
]);
if (rows.length === 0) {
logger.warn(`DNS record not found for ${domain}`);
return;
}
const cloudflareID = String(rows[0][0]);
// Delete from Cloudflare
if (cloudflareID) {
await this.cloudflareClient!.zones.dns.records.delete(this.zoneID!, cloudflareID);
}
// Delete from database
this.database.query('DELETE FROM dns_records WHERE domain = ?', [domain]);
logger.success(`DNS record removed for ${domain}`);
} catch (error) {
logger.error(`Failed to remove DNS record for ${domain}: ${error.message}`);
throw error;
}
}
/**
* Get DNS record for a domain
*/
async getDNSRecord(domain: string): Promise<any> {
try {
if (!this.isConfigured()) {
return null;
}
// Get from database first
const rows = this.database.query('SELECT * FROM dns_records WHERE domain = ?', [domain]);
if (rows.length > 0) {
return {
domain: String(rows[0][1]),
type: String(rows[0][2]),
value: String(rows[0][3]),
cloudflareID: rows[0][4] ? String(rows[0][4]) : null,
};
}
return null;
} catch (error) {
logger.error(`Failed to get DNS record for ${domain}: ${error.message}`);
return null;
}
}
/**
* List all DNS records
*/
listDNSRecords(): any[] {
try {
const rows = this.database.query('SELECT * FROM dns_records ORDER BY created_at DESC');
return rows.map((row) => ({
id: Number(row[0]),
domain: String(row[1]),
type: String(row[2]),
value: String(row[3]),
cloudflareID: row[4] ? String(row[4]) : null,
createdAt: Number(row[5]),
updatedAt: Number(row[6]),
}));
} catch (error) {
logger.error(`Failed to list DNS records: ${error.message}`);
return [];
}
}
/**
* Sync DNS records from Cloudflare
*/
async syncFromCloudflare(): Promise<void> {
try {
if (!this.isConfigured()) {
throw new Error('DNS manager not configured');
}
logger.info('Syncing DNS records from Cloudflare...');
const response = await this.cloudflareClient!.zones.dns.records.list(this.zoneID!);
const records = response.result;
// Only sync A records
const aRecords = records.filter((r: any) => r.type === 'A');
for (const record of aRecords) {
// Check if exists in database
const existing = await this.getDNSRecord(record.name);
if (!existing) {
// Add to database
await this.database.query(
'INSERT INTO dns_records (domain, type, value, cloudflare_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[record.name, record.type, record.content, record.id, Date.now(), Date.now()]
);
logger.info(`Synced DNS record: ${record.name}`);
}
}
logger.success('DNS records synced from Cloudflare');
} catch (error) {
logger.error(`Failed to sync DNS records: ${error.message}`);
throw error;
}
}
/**
* Check if domain DNS is properly configured
*/
async checkDNS(domain: string): Promise<boolean> {
try {
logger.info(`Checking DNS for ${domain}...`);
// Use dig or nslookup to check DNS resolution
const command = new Deno.Command('dig', {
args: ['+short', domain],
stdout: 'piped',
stderr: 'piped',
});
const { code, stdout } = await command.output();
if (code !== 0) {
logger.warn(`DNS check failed for ${domain}`);
return false;
}
const ip = new TextDecoder().decode(stdout).trim();
if (ip === this.serverIP) {
logger.success(`DNS correctly points to ${ip}`);
return true;
} else {
logger.warn(`DNS points to ${ip}, expected ${this.serverIP}`);
return false;
}
} catch (error) {
logger.error(`Failed to check DNS for ${domain}: ${error.message}`);
return false;
}
}
}

489
ts/onebox.classes.docker.ts Normal file
View File

@@ -0,0 +1,489 @@
/**
* Docker Manager for Onebox
*
* Handles all Docker operations: containers, images, networks, volumes
*/
import * as plugins from './onebox.plugins.ts';
import type { IService, IContainerStats } from './onebox.types.ts';
import { logger } from './onebox.logging.ts';
export class OneboxDockerManager {
private dockerClient: plugins.docker.Docker | null = null;
private networkName = 'onebox-network';
/**
* Initialize Docker client and create onebox network
*/
async init(): Promise<void> {
try {
// Initialize Docker client (connects to /var/run/docker.sock by default)
this.dockerClient = new plugins.docker.Docker({
socketPath: '/var/run/docker.sock',
});
logger.info('Docker client initialized');
// Ensure onebox network exists
await this.ensureNetwork();
} catch (error) {
logger.error(`Failed to initialize Docker client: ${error.message}`);
throw error;
}
}
/**
* Ensure onebox network exists
*/
private async ensureNetwork(): Promise<void> {
try {
const networks = await this.dockerClient!.listNetworks();
const existingNetwork = networks.find((n: any) => n.Name === this.networkName);
if (!existingNetwork) {
logger.info(`Creating Docker network: ${this.networkName}`);
await this.dockerClient!.createNetwork({
Name: this.networkName,
Driver: 'bridge',
Labels: {
'managed-by': 'onebox',
},
});
logger.success(`Docker network created: ${this.networkName}`);
} else {
logger.debug(`Docker network already exists: ${this.networkName}`);
}
} catch (error) {
logger.error(`Failed to create Docker network: ${error.message}`);
throw error;
}
}
/**
* Pull an image from a registry
*/
async pullImage(image: string, registry?: string): Promise<void> {
try {
logger.info(`Pulling Docker image: ${image}`);
const fullImage = registry ? `${registry}/${image}` : image;
await this.dockerClient!.pull(fullImage, (error: any, stream: any) => {
if (error) {
throw error;
}
// Follow progress
this.dockerClient!.modem.followProgress(stream, (err: any, output: any) => {
if (err) {
throw err;
}
logger.debug('Pull complete:', output);
});
});
logger.success(`Image pulled successfully: ${fullImage}`);
} catch (error) {
logger.error(`Failed to pull image ${image}: ${error.message}`);
throw error;
}
}
/**
* Create and start a container
*/
async createContainer(service: IService): Promise<string> {
try {
logger.info(`Creating container for service: ${service.name}`);
const fullImage = service.registry
? `${service.registry}/${service.image}`
: service.image;
// Prepare environment variables
const env: string[] = [];
for (const [key, value] of Object.entries(service.envVars)) {
env.push(`${key}=${value}`);
}
// Create container
const container = await this.dockerClient!.createContainer({
Image: fullImage,
name: `onebox-${service.name}`,
Env: env,
Labels: {
'managed-by': 'onebox',
'onebox-service': service.name,
},
ExposedPorts: {
[`${service.port}/tcp`]: {},
},
HostConfig: {
NetworkMode: this.networkName,
RestartPolicy: {
Name: 'unless-stopped',
},
PortBindings: {
// Don't bind to host ports - nginx will proxy
[`${service.port}/tcp`]: [],
},
},
});
const containerID = container.id;
logger.success(`Container created: ${containerID}`);
return containerID;
} catch (error) {
logger.error(`Failed to create container for ${service.name}: ${error.message}`);
throw error;
}
}
/**
* Start a container by ID
*/
async startContainer(containerID: string): Promise<void> {
try {
logger.info(`Starting container: ${containerID}`);
const container = this.dockerClient!.getContainer(containerID);
await container.start();
logger.success(`Container started: ${containerID}`);
} catch (error) {
// Ignore "already started" errors
if (error.message.includes('already started')) {
logger.debug(`Container already running: ${containerID}`);
return;
}
logger.error(`Failed to start container ${containerID}: ${error.message}`);
throw error;
}
}
/**
* Stop a container by ID
*/
async stopContainer(containerID: string): Promise<void> {
try {
logger.info(`Stopping container: ${containerID}`);
const container = this.dockerClient!.getContainer(containerID);
await container.stop();
logger.success(`Container stopped: ${containerID}`);
} catch (error) {
// Ignore "already stopped" errors
if (error.message.includes('already stopped') || error.statusCode === 304) {
logger.debug(`Container already stopped: ${containerID}`);
return;
}
logger.error(`Failed to stop container ${containerID}: ${error.message}`);
throw error;
}
}
/**
* Restart a container by ID
*/
async restartContainer(containerID: string): Promise<void> {
try {
logger.info(`Restarting container: ${containerID}`);
const container = this.dockerClient!.getContainer(containerID);
await container.restart();
logger.success(`Container restarted: ${containerID}`);
} catch (error) {
logger.error(`Failed to restart container ${containerID}: ${error.message}`);
throw error;
}
}
/**
* Remove a container by ID
*/
async removeContainer(containerID: string, force = false): Promise<void> {
try {
logger.info(`Removing container: ${containerID}`);
const container = this.dockerClient!.getContainer(containerID);
// Stop first if not forced
if (!force) {
try {
await this.stopContainer(containerID);
} catch (error) {
// Ignore stop errors
logger.debug(`Error stopping container before removal: ${error.message}`);
}
}
await container.remove({ force });
logger.success(`Container removed: ${containerID}`);
} catch (error) {
logger.error(`Failed to remove container ${containerID}: ${error.message}`);
throw error;
}
}
/**
* Get container status
*/
async getContainerStatus(containerID: string): Promise<string> {
try {
const container = this.dockerClient!.getContainer(containerID);
const info = await container.inspect();
return info.State.Status;
} catch (error) {
logger.error(`Failed to get container status ${containerID}: ${error.message}`);
return 'unknown';
}
}
/**
* Get container stats (CPU, memory, network)
*/
async getContainerStats(containerID: string): Promise<IContainerStats | null> {
try {
const container = this.dockerClient!.getContainer(containerID);
const stats = await container.stats({ stream: false });
// Calculate CPU percentage
const cpuDelta =
stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
const cpuPercent =
systemDelta > 0 ? (cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100 : 0;
// Memory stats
const memoryUsed = stats.memory_stats.usage || 0;
const memoryLimit = stats.memory_stats.limit || 0;
const memoryPercent = memoryLimit > 0 ? (memoryUsed / memoryLimit) * 100 : 0;
// Network stats
let networkRx = 0;
let networkTx = 0;
if (stats.networks) {
for (const network of Object.values(stats.networks)) {
networkRx += (network as any).rx_bytes || 0;
networkTx += (network as any).tx_bytes || 0;
}
}
return {
cpuPercent,
memoryUsed,
memoryLimit,
memoryPercent,
networkRx,
networkTx,
};
} catch (error) {
logger.error(`Failed to get container stats ${containerID}: ${error.message}`);
return null;
}
}
/**
* Get container logs
*/
async getContainerLogs(
containerID: string,
tail = 100
): Promise<{ stdout: string; stderr: string }> {
try {
const container = this.dockerClient!.getContainer(containerID);
const logs = await container.logs({
stdout: true,
stderr: true,
tail,
timestamps: true,
});
// Parse logs (Docker returns them in a special format)
const stdout: string[] = [];
const stderr: string[] = [];
const lines = logs.toString().split('\n');
for (const line of lines) {
if (line.length === 0) continue;
// Docker log format: first byte indicates stream (1=stdout, 2=stderr)
const streamType = line.charCodeAt(0);
const content = line.slice(8); // Skip header (8 bytes)
if (streamType === 1) {
stdout.push(content);
} else if (streamType === 2) {
stderr.push(content);
}
}
return {
stdout: stdout.join('\n'),
stderr: stderr.join('\n'),
};
} catch (error) {
logger.error(`Failed to get container logs ${containerID}: ${error.message}`);
return { stdout: '', stderr: '' };
}
}
/**
* Stream container logs (real-time)
*/
async streamContainerLogs(
containerID: string,
callback: (line: string, isError: boolean) => void
): Promise<void> {
try {
const container = this.dockerClient!.getContainer(containerID);
const stream = await container.logs({
stdout: true,
stderr: true,
follow: true,
tail: 0,
timestamps: true,
});
stream.on('data', (chunk: Buffer) => {
const streamType = chunk[0];
const content = chunk.slice(8).toString();
callback(content, streamType === 2);
});
stream.on('error', (error: Error) => {
logger.error(`Log stream error for ${containerID}: ${error.message}`);
});
} catch (error) {
logger.error(`Failed to stream container logs ${containerID}: ${error.message}`);
throw error;
}
}
/**
* List all onebox-managed containers
*/
async listContainers(): Promise<any[]> {
try {
const containers = await this.dockerClient!.listContainers({
all: true,
filters: {
label: ['managed-by=onebox'],
},
});
return containers;
} catch (error) {
logger.error(`Failed to list containers: ${error.message}`);
return [];
}
}
/**
* Check if Docker is running
*/
async isDockerRunning(): Promise<boolean> {
try {
await this.dockerClient!.ping();
return true;
} catch (error) {
return false;
}
}
/**
* Get Docker version info
*/
async getDockerVersion(): Promise<any> {
try {
return await this.dockerClient!.version();
} catch (error) {
logger.error(`Failed to get Docker version: ${error.message}`);
return null;
}
}
/**
* Prune unused images
*/
async pruneImages(): Promise<void> {
try {
logger.info('Pruning unused Docker images...');
await this.dockerClient!.pruneImages();
logger.success('Unused images pruned successfully');
} catch (error) {
logger.error(`Failed to prune images: ${error.message}`);
throw error;
}
}
/**
* Get container IP address in onebox network
*/
async getContainerIP(containerID: string): Promise<string | null> {
try {
const container = this.dockerClient!.getContainer(containerID);
const info = await container.inspect();
const networks = info.NetworkSettings.Networks;
if (networks && networks[this.networkName]) {
return networks[this.networkName].IPAddress;
}
return null;
} catch (error) {
logger.error(`Failed to get container IP ${containerID}: ${error.message}`);
return null;
}
}
/**
* Execute a command in a running container
*/
async execInContainer(
containerID: string,
cmd: string[]
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
try {
const container = this.dockerClient!.getContainer(containerID);
const exec = await container.exec({
Cmd: cmd,
AttachStdout: true,
AttachStderr: true,
});
const stream = await exec.start({ Detach: false });
let stdout = '';
let stderr = '';
stream.on('data', (chunk: Buffer) => {
const streamType = chunk[0];
const content = chunk.slice(8).toString();
if (streamType === 1) {
stdout += content;
} else if (streamType === 2) {
stderr += content;
}
});
// Wait for completion
await new Promise((resolve) => stream.on('end', resolve));
const inspect = await exec.inspect();
const exitCode = inspect.ExitCode || 0;
return { stdout, stderr, exitCode };
} catch (error) {
logger.error(`Failed to exec in container ${containerID}: ${error.message}`);
throw error;
}
}
}

View File

@@ -0,0 +1,193 @@
/**
* HTTP Server for Onebox
*
* Serves REST API and Angular UI
*/
import * as plugins from './onebox.plugins.ts';
import { logger } from './onebox.logging.ts';
import type { Onebox } from './onebox.classes.onebox.ts';
import type { IApiResponse } from './onebox.types.ts';
export class OneboxHttpServer {
private oneboxRef: Onebox;
private server: Deno.HttpServer | null = null;
private port = 3000;
constructor(oneboxRef: Onebox) {
this.oneboxRef = oneboxRef;
}
/**
* Start HTTP server
*/
async start(port?: number): Promise<void> {
try {
if (this.server) {
logger.warn('HTTP server already running');
return;
}
this.port = port || 3000;
logger.info(`Starting HTTP server on port ${this.port}...`);
this.server = Deno.serve({ port: this.port }, (req) => this.handleRequest(req));
logger.success(`HTTP server started on http://localhost:${this.port}`);
} catch (error) {
logger.error(`Failed to start HTTP server: ${error.message}`);
throw error;
}
}
/**
* Stop HTTP server
*/
async stop(): Promise<void> {
try {
if (!this.server) {
return;
}
logger.info('Stopping HTTP server...');
await this.server.shutdown();
this.server = null;
logger.success('HTTP server stopped');
} catch (error) {
logger.error(`Failed to stop HTTP server: ${error.message}`);
throw error;
}
}
/**
* Handle HTTP request
*/
private async handleRequest(req: Request): Promise<Response> {
const url = new URL(req.url);
const path = url.pathname;
logger.debug(`${req.method} ${path}`);
try {
// API routes
if (path.startsWith('/api/')) {
return await this.handleApiRequest(req, path);
}
// Serve Angular UI (TODO: implement static file serving)
return new Response('Onebox API - UI coming soon', {
headers: { 'Content-Type': 'text/plain' },
});
} catch (error) {
logger.error(`Request error: ${error.message}`);
return this.jsonResponse({ success: false, error: error.message }, 500);
}
}
/**
* Handle API requests
*/
private async handleApiRequest(req: Request, path: string): Promise<Response> {
const method = req.method;
// Auth check (simplified - should use proper JWT middleware)
// Skip auth for login endpoint
if (path !== '/api/auth/login') {
const authHeader = req.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return this.jsonResponse({ success: false, error: 'Unauthorized' }, 401);
}
}
// Route to appropriate handler
if (path === '/api/status' && method === 'GET') {
return await this.handleStatusRequest();
} else if (path === '/api/services' && method === 'GET') {
return await this.handleListServicesRequest();
} else if (path === '/api/services' && method === 'POST') {
return await this.handleDeployServiceRequest(req);
} else if (path.match(/^\/api\/services\/[^/]+$/) && method === 'GET') {
const name = path.split('/').pop()!;
return await this.handleGetServiceRequest(name);
} else if (path.match(/^\/api\/services\/[^/]+$/) && method === 'DELETE') {
const name = path.split('/').pop()!;
return await this.handleDeleteServiceRequest(name);
} else if (path.match(/^\/api\/services\/[^/]+\/start$/) && method === 'POST') {
const name = path.split('/')[3];
return await this.handleStartServiceRequest(name);
} else if (path.match(/^\/api\/services\/[^/]+\/stop$/) && method === 'POST') {
const name = path.split('/')[3];
return await this.handleStopServiceRequest(name);
} else if (path.match(/^\/api\/services\/[^/]+\/restart$/) && method === 'POST') {
const name = path.split('/')[3];
return await this.handleRestartServiceRequest(name);
} else if (path.match(/^\/api\/services\/[^/]+\/logs$/) && method === 'GET') {
const name = path.split('/')[3];
return await this.handleGetLogsRequest(name);
} else {
return this.jsonResponse({ success: false, error: 'Not found' }, 404);
}
}
// API Handlers
private async handleStatusRequest(): Promise<Response> {
const status = await this.oneboxRef.getSystemStatus();
return this.jsonResponse({ success: true, data: status });
}
private async handleListServicesRequest(): Promise<Response> {
const services = this.oneboxRef.services.listServices();
return this.jsonResponse({ success: true, data: services });
}
private async handleDeployServiceRequest(req: Request): Promise<Response> {
const body = await req.json();
const service = await this.oneboxRef.services.deployService(body);
return this.jsonResponse({ success: true, data: service });
}
private async handleGetServiceRequest(name: string): Promise<Response> {
const service = this.oneboxRef.services.getService(name);
if (!service) {
return this.jsonResponse({ success: false, error: 'Service not found' }, 404);
}
return this.jsonResponse({ success: true, data: service });
}
private async handleDeleteServiceRequest(name: string): Promise<Response> {
await this.oneboxRef.services.removeService(name);
return this.jsonResponse({ success: true, message: 'Service removed' });
}
private async handleStartServiceRequest(name: string): Promise<Response> {
await this.oneboxRef.services.startService(name);
return this.jsonResponse({ success: true, message: 'Service started' });
}
private async handleStopServiceRequest(name: string): Promise<Response> {
await this.oneboxRef.services.stopService(name);
return this.jsonResponse({ success: true, message: 'Service stopped' });
}
private async handleRestartServiceRequest(name: string): Promise<Response> {
await this.oneboxRef.services.restartService(name);
return this.jsonResponse({ success: true, message: 'Service restarted' });
}
private async handleGetLogsRequest(name: string): Promise<Response> {
const logs = await this.oneboxRef.services.getServiceLogs(name);
return this.jsonResponse({ success: true, data: logs });
}
/**
* Helper to create JSON response
*/
private jsonResponse(data: IApiResponse, status = 200): Response {
return new Response(JSON.stringify(data), {
status,
headers: { 'Content-Type': 'application/json' },
});
}
}

345
ts/onebox.classes.nginx.ts Normal file
View File

@@ -0,0 +1,345 @@
/**
* Nginx Manager for Onebox
*
* Manages Nginx reverse proxy configurations for services
*/
import * as plugins from './onebox.plugins.ts';
import { logger } from './onebox.logging.ts';
import { OneboxDatabase } from './onebox.classes.database.ts';
export class OneboxNginxManager {
private oneboxRef: any;
private database: OneboxDatabase;
private configDir = '/etc/nginx/sites-available';
private enabledDir = '/etc/nginx/sites-enabled';
constructor(oneboxRef: any) {
this.oneboxRef = oneboxRef;
this.database = oneboxRef.database;
// Allow custom nginx config directory
const customDir = this.database.getSetting('nginxConfigDir');
if (customDir) {
this.configDir = customDir;
}
}
/**
* Initialize nginx manager
*/
async init(): Promise<void> {
try {
// Ensure directories exist
await Deno.mkdir(this.configDir, { recursive: true });
await Deno.mkdir(this.enabledDir, { recursive: true });
logger.info('Nginx manager initialized');
} catch (error) {
logger.error(`Failed to initialize Nginx manager: ${error.message}`);
throw error;
}
}
/**
* Create nginx config for a service
*/
async createConfig(serviceId: number, domain: string, port: number): Promise<void> {
try {
logger.info(`Creating Nginx config for ${domain}`);
const service = this.database.getServiceByID(serviceId);
if (!service) {
throw new Error(`Service not found: ${serviceId}`);
}
// Get container IP (or use container name for DNS resolution within Docker network)
const containerName = `onebox-${service.name}`;
// Generate config
const config = this.generateConfig(domain, containerName, port);
// Write config file
const configPath = `${this.configDir}/onebox-${service.name}.conf`;
await Deno.writeTextFile(configPath, config);
// Create symlink in sites-enabled
const enabledPath = `${this.enabledDir}/onebox-${service.name}.conf`;
try {
await Deno.remove(enabledPath);
} catch {
// Ignore if doesn't exist
}
await Deno.symlink(configPath, enabledPath);
logger.success(`Nginx config created: ${domain}`);
} catch (error) {
logger.error(`Failed to create Nginx config: ${error.message}`);
throw error;
}
}
/**
* Generate nginx configuration
*/
private generateConfig(domain: string, upstream: string, port: number): string {
return `# Onebox-managed configuration for ${domain}
# Generated at ${new Date().toISOString()}
upstream onebox_${domain.replace(/\./g, '_')} {
server ${upstream}:${port};
}
# HTTP server (redirect to HTTPS or serve directly)
server {
listen 80;
listen [::]:80;
server_name ${domain};
# ACME challenge for Let's Encrypt
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Redirect to HTTPS (will be enabled after SSL is configured)
# location / {
# return 301 https://$server_name$request_uri;
# }
# Proxy to container (remove after SSL is configured)
location / {
proxy_pass http://onebox_${domain.replace(/\./g, '_')};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
# HTTPS server (uncomment after SSL is configured)
# server {
# listen 443 ssl http2;
# listen [::]:443 ssl http2;
# server_name ${domain};
#
# ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
#
# # SSL configuration
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
#
# location / {
# proxy_pass http://onebox_${domain.replace(/\./g, '_')};
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
#
# # WebSocket support
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
#
# # Timeouts
# proxy_connect_timeout 60s;
# proxy_send_timeout 60s;
# proxy_read_timeout 60s;
# }
# }
`;
}
/**
* Update nginx config to enable SSL
*/
async enableSSL(domain: string): Promise<void> {
try {
logger.info(`Enabling SSL for ${domain}`);
// Find service by domain
const services = this.database.getAllServices();
const service = services.find((s) => s.domain === domain);
if (!service) {
throw new Error(`Service not found for domain: ${domain}`);
}
const configPath = `${this.configDir}/onebox-${service.name}.conf`;
let config = await Deno.readTextFile(configPath);
// Enable HTTPS redirect and HTTPS server block
config = config.replace('# location / {\n # return 301', 'location / {\n return 301');
config = config.replace('# }', '}');
config = config.replace(/# server \{[\s\S]*?# \}/m, (match) =>
match.replace(/# /g, '')
);
// Comment out HTTP proxy location
config = config.replace(
/# Proxy to container \(remove after SSL is configured\)[\s\S]*?location \/ \{[\s\S]*?\n \}/,
(match) => `# ${match.replace(/\n/g, '\n # ')}`
);
await Deno.writeTextFile(configPath, config);
logger.success(`SSL enabled for ${domain}`);
} catch (error) {
logger.error(`Failed to enable SSL for ${domain}: ${error.message}`);
throw error;
}
}
/**
* Remove nginx config for a service
*/
async removeConfig(serviceId: number): Promise<void> {
try {
const service = this.database.getServiceByID(serviceId);
if (!service) {
throw new Error(`Service not found: ${serviceId}`);
}
logger.info(`Removing Nginx config for ${service.name}`);
// Remove symlink
const enabledPath = `${this.enabledDir}/onebox-${service.name}.conf`;
try {
await Deno.remove(enabledPath);
} catch {
// Ignore if doesn't exist
}
// Remove config file
const configPath = `${this.configDir}/onebox-${service.name}.conf`;
try {
await Deno.remove(configPath);
} catch {
// Ignore if doesn't exist
}
logger.success(`Nginx config removed for ${service.name}`);
} catch (error) {
logger.error(`Failed to remove Nginx config: ${error.message}`);
throw error;
}
}
/**
* Test nginx configuration
*/
async test(): Promise<boolean> {
try {
const command = new Deno.Command('nginx', {
args: ['-t'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stderr } = await command.output();
if (code !== 0) {
const errorMsg = new TextDecoder().decode(stderr);
logger.error(`Nginx config test failed: ${errorMsg}`);
return false;
}
logger.success('Nginx configuration is valid');
return true;
} catch (error) {
logger.error(`Failed to test Nginx config: ${error.message}`);
return false;
}
}
/**
* Reload nginx
*/
async reload(): Promise<void> {
try {
// Test config first
const isValid = await this.test();
if (!isValid) {
throw new Error('Nginx configuration is invalid');
}
logger.info('Reloading Nginx...');
const command = new Deno.Command('systemctl', {
args: ['reload', 'nginx'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stderr } = await command.output();
if (code !== 0) {
const errorMsg = new TextDecoder().decode(stderr);
throw new Error(`Nginx reload failed: ${errorMsg}`);
}
logger.success('Nginx reloaded successfully');
} catch (error) {
logger.error(`Failed to reload Nginx: ${error.message}`);
throw error;
}
}
/**
* Get nginx status
*/
async getStatus(): Promise<string> {
try {
const command = new Deno.Command('systemctl', {
args: ['status', 'nginx'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stdout } = await command.output();
const output = new TextDecoder().decode(stdout);
if (code === 0 || output.includes('active (running)')) {
return 'running';
} else if (output.includes('inactive') || output.includes('dead')) {
return 'stopped';
} else if (output.includes('failed')) {
return 'failed';
} else {
return 'unknown';
}
} catch (error) {
logger.error(`Failed to get Nginx status: ${error.message}`);
return 'unknown';
}
}
/**
* Check if nginx is installed
*/
async isInstalled(): Promise<boolean> {
try {
const command = new Deno.Command('which', {
args: ['nginx'],
stdout: 'piped',
stderr: 'piped',
});
const { code } = await command.output();
return code === 0;
} catch {
return false;
}
}
}

220
ts/onebox.classes.onebox.ts Normal file
View File

@@ -0,0 +1,220 @@
/**
* Main Onebox coordinator class
*
* Coordinates all components and provides the main API
*/
import { logger } from './onebox.logging.ts';
import { OneboxDatabase } from './onebox.classes.database.ts';
import { OneboxDockerManager } from './onebox.classes.docker.ts';
import { OneboxServicesManager } from './onebox.classes.services.ts';
import { OneboxRegistriesManager } from './onebox.classes.registries.ts';
import { OneboxNginxManager } from './onebox.classes.nginx.ts';
import { OneboxDnsManager } from './onebox.classes.dns.ts';
import { OneboxSslManager } from './onebox.classes.ssl.ts';
import { OneboxDaemon } from './onebox.classes.daemon.ts';
import { OneboxHttpServer } from './onebox.classes.httpserver.ts';
export class Onebox {
public database: OneboxDatabase;
public docker: OneboxDockerManager;
public services: OneboxServicesManager;
public registries: OneboxRegistriesManager;
public nginx: OneboxNginxManager;
public dns: OneboxDnsManager;
public ssl: OneboxSslManager;
public daemon: OneboxDaemon;
public httpServer: OneboxHttpServer;
private initialized = false;
constructor() {
// Initialize database first
this.database = new OneboxDatabase();
// Initialize managers (passing reference to main Onebox instance)
this.docker = new OneboxDockerManager();
this.services = new OneboxServicesManager(this);
this.registries = new OneboxRegistriesManager(this);
this.nginx = new OneboxNginxManager(this);
this.dns = new OneboxDnsManager(this);
this.ssl = new OneboxSslManager(this);
this.daemon = new OneboxDaemon(this);
this.httpServer = new OneboxHttpServer(this);
}
/**
* Initialize all components
*/
async init(): Promise<void> {
try {
logger.info('Initializing Onebox...');
// Initialize database
await this.database.init();
// Ensure default admin user exists
await this.ensureDefaultUser();
// Initialize Docker
await this.docker.init();
// Initialize Nginx
await this.nginx.init();
// Initialize DNS (non-critical)
try {
await this.dns.init();
} catch (error) {
logger.warn('DNS initialization failed - DNS features will be disabled');
}
// Initialize SSL (non-critical)
try {
await this.ssl.init();
} catch (error) {
logger.warn('SSL initialization failed - SSL features will be limited');
}
// Login to all registries
await this.registries.loginToAllRegistries();
this.initialized = true;
logger.success('Onebox initialized successfully');
} catch (error) {
logger.error(`Failed to initialize Onebox: ${error.message}`);
throw error;
}
}
/**
* Ensure default admin user exists
*/
private async ensureDefaultUser(): Promise<void> {
try {
const adminUser = this.database.getUserByUsername('admin');
if (!adminUser) {
logger.info('Creating default admin user...');
// Hash default password 'admin'
const passwordHash = await Deno.readTextFile('/dev/urandom').then((data) =>
// Simple hash for now - should use bcrypt
btoa('admin')
);
await this.database.createUser({
username: 'admin',
passwordHash: btoa('admin'), // Simple encoding for now
role: 'admin',
createdAt: Date.now(),
updatedAt: Date.now(),
});
logger.warn('Default admin user created with username: admin, password: admin');
logger.warn('IMPORTANT: Change the default password immediately!');
}
} catch (error) {
logger.error(`Failed to create default user: ${error.message}`);
}
}
/**
* Check if Onebox is initialized
*/
isInitialized(): boolean {
return this.initialized;
}
/**
* Get system status
*/
async getSystemStatus() {
try {
const dockerRunning = await this.docker.isDockerRunning();
const nginxStatus = await this.nginx.getStatus();
const dnsConfigured = this.dns.isConfigured();
const sslConfigured = this.ssl.isConfigured();
const services = this.services.listServices();
const runningServices = services.filter((s) => s.status === 'running').length;
const totalServices = services.length;
return {
docker: {
running: dockerRunning,
version: dockerRunning ? await this.docker.getDockerVersion() : null,
},
nginx: {
status: nginxStatus,
installed: await this.nginx.isInstalled(),
},
dns: {
configured: dnsConfigured,
},
ssl: {
configured: sslConfigured,
certbotInstalled: await this.ssl.isCertbotInstalled(),
},
services: {
total: totalServices,
running: runningServices,
stopped: totalServices - runningServices,
},
};
} catch (error) {
logger.error(`Failed to get system status: ${error.message}`);
throw error;
}
}
/**
* Start daemon mode
*/
async startDaemon(): Promise<void> {
await this.daemon.start();
}
/**
* Stop daemon mode
*/
async stopDaemon(): Promise<void> {
await this.daemon.stop();
}
/**
* Start HTTP server
*/
async startHttpServer(port?: number): Promise<void> {
await this.httpServer.start(port);
}
/**
* Stop HTTP server
*/
async stopHttpServer(): Promise<void> {
await this.httpServer.stop();
}
/**
* Shutdown Onebox gracefully
*/
async shutdown(): Promise<void> {
try {
logger.info('Shutting down Onebox...');
// Stop daemon if running
await this.daemon.stop();
// Stop HTTP server if running
await this.httpServer.stop();
// Close database
this.database.close();
logger.success('Onebox shutdown complete');
} catch (error) {
logger.error(`Error during shutdown: ${error.message}`);
}
}
}

View File

@@ -0,0 +1,195 @@
/**
* Registry Manager for Onebox
*
* Manages Docker registry credentials and authentication
*/
import * as plugins from './onebox.plugins.ts';
import type { IRegistry } from './onebox.types.ts';
import { logger } from './onebox.logging.ts';
import { OneboxDatabase } from './onebox.classes.database.ts';
export class OneboxRegistriesManager {
private oneboxRef: any; // Will be Onebox instance
private database: OneboxDatabase;
constructor(oneboxRef: any) {
this.oneboxRef = oneboxRef;
this.database = oneboxRef.database;
}
/**
* Encrypt a password (simple base64 for now, should use proper encryption)
*/
private encryptPassword(password: string): string {
// TODO: Use proper encryption with a secret key
// For now, using base64 encoding (NOT SECURE, just for structure)
return plugins.encoding.encodeBase64(new TextEncoder().encode(password));
}
/**
* Decrypt a password
*/
private decryptPassword(encrypted: string): string {
// TODO: Use proper decryption
return new TextDecoder().decode(plugins.encoding.decodeBase64(encrypted));
}
/**
* Add a registry
*/
async addRegistry(url: string, username: string, password: string): Promise<IRegistry> {
try {
// Check if registry already exists
const existing = this.database.getRegistryByURL(url);
if (existing) {
throw new Error(`Registry already exists: ${url}`);
}
// Encrypt password
const passwordEncrypted = this.encryptPassword(password);
// Create registry in database
const registry = await this.database.createRegistry({
url,
username,
passwordEncrypted,
createdAt: Date.now(),
});
logger.success(`Registry added: ${url}`);
// Perform Docker login
await this.loginToRegistry(registry);
return registry;
} catch (error) {
logger.error(`Failed to add registry ${url}: ${error.message}`);
throw error;
}
}
/**
* Remove a registry
*/
async removeRegistry(url: string): Promise<void> {
try {
const registry = this.database.getRegistryByURL(url);
if (!registry) {
throw new Error(`Registry not found: ${url}`);
}
this.database.deleteRegistry(url);
logger.success(`Registry removed: ${url}`);
// Note: We don't perform docker logout as it might affect other users
} catch (error) {
logger.error(`Failed to remove registry ${url}: ${error.message}`);
throw error;
}
}
/**
* List all registries
*/
listRegistries(): IRegistry[] {
return this.database.getAllRegistries();
}
/**
* Get registry by URL
*/
getRegistry(url: string): IRegistry | null {
return this.database.getRegistryByURL(url);
}
/**
* Perform Docker login for a registry
*/
async loginToRegistry(registry: IRegistry): Promise<void> {
try {
logger.info(`Logging into registry: ${registry.url}`);
const password = this.decryptPassword(registry.passwordEncrypted);
// Use docker login command
const command = [
'docker',
'login',
registry.url,
'--username',
registry.username,
'--password-stdin',
];
const process = new Deno.Command('docker', {
args: ['login', registry.url, '--username', registry.username, '--password-stdin'],
stdin: 'piped',
stdout: 'piped',
stderr: 'piped',
});
const child = process.spawn();
// Write password to stdin
const writer = child.stdin.getWriter();
await writer.write(new TextEncoder().encode(password));
await writer.close();
const { code, stdout, stderr } = await child.output();
if (code !== 0) {
const errorMsg = new TextDecoder().decode(stderr);
throw new Error(`Docker login failed: ${errorMsg}`);
}
logger.success(`Logged into registry: ${registry.url}`);
} catch (error) {
logger.error(`Failed to login to registry ${registry.url}: ${error.message}`);
throw error;
}
}
/**
* Login to all registries (useful on daemon start)
*/
async loginToAllRegistries(): Promise<void> {
const registries = this.listRegistries();
for (const registry of registries) {
try {
await this.loginToRegistry(registry);
} catch (error) {
logger.warn(`Failed to login to ${registry.url}: ${error.message}`);
// Continue with other registries
}
}
}
/**
* Test registry connection
*/
async testRegistry(url: string, username: string, password: string): Promise<boolean> {
try {
const command = new Deno.Command('docker', {
args: ['login', url, '--username', username, '--password-stdin'],
stdin: 'piped',
stdout: 'piped',
stderr: 'piped',
});
const child = command.spawn();
const writer = child.stdin.getWriter();
await writer.write(new TextEncoder().encode(password));
await writer.close();
const { code } = await child.output();
return code === 0;
} catch (error) {
logger.error(`Failed to test registry ${url}: ${error.message}`);
return false;
}
}
}

View File

@@ -0,0 +1,407 @@
/**
* Services Manager for Onebox
*
* Orchestrates service deployment: Docker + Nginx + DNS + SSL
*/
import type { IService, IServiceDeployOptions } from './onebox.types.ts';
import { logger } from './onebox.logging.ts';
import { OneboxDatabase } from './onebox.classes.database.ts';
import { OneboxDockerManager } from './onebox.classes.docker.ts';
export class OneboxServicesManager {
private oneboxRef: any; // Will be Onebox instance
private database: OneboxDatabase;
private docker: OneboxDockerManager;
constructor(oneboxRef: any) {
this.oneboxRef = oneboxRef;
this.database = oneboxRef.database;
this.docker = oneboxRef.docker;
}
/**
* Deploy a new service (full workflow)
*/
async deployService(options: IServiceDeployOptions): Promise<IService> {
try {
logger.info(`Deploying service: ${options.name}`);
// Check if service already exists
const existing = this.database.getServiceByName(options.name);
if (existing) {
throw new Error(`Service already exists: ${options.name}`);
}
// Create service record in database
const service = await this.database.createService({
name: options.name,
image: options.image,
registry: options.registry,
envVars: options.envVars || {},
port: options.port,
domain: options.domain,
status: 'stopped',
createdAt: Date.now(),
updatedAt: Date.now(),
});
// Pull image
await this.docker.pullImage(options.image, options.registry);
// Create container
const containerID = await this.docker.createContainer(service);
// Update service with container ID
this.database.updateService(service.id!, {
containerID,
status: 'starting',
});
// Start container
await this.docker.startContainer(containerID);
// Update status
this.database.updateService(service.id!, { status: 'running' });
// If domain is specified, configure nginx, DNS, and SSL
if (options.domain) {
logger.info(`Configuring domain: ${options.domain}`);
// Configure DNS (if autoDNS is enabled)
if (options.autoDNS !== false) {
try {
await this.oneboxRef.dns.addDNSRecord(options.domain);
} catch (error) {
logger.warn(`Failed to configure DNS for ${options.domain}: ${error.message}`);
}
}
// Configure nginx
try {
await this.oneboxRef.nginx.createConfig(service.id!, options.domain, options.port);
await this.oneboxRef.nginx.reload();
} catch (error) {
logger.warn(`Failed to configure Nginx for ${options.domain}: ${error.message}`);
}
// Configure SSL (if autoSSL is enabled)
if (options.autoSSL !== false) {
try {
await this.oneboxRef.ssl.obtainCertificate(options.domain);
await this.oneboxRef.nginx.reload();
} catch (error) {
logger.warn(`Failed to obtain SSL certificate for ${options.domain}: ${error.message}`);
}
}
}
logger.success(`Service deployed successfully: ${options.name}`);
return this.database.getServiceByName(options.name)!;
} catch (error) {
logger.error(`Failed to deploy service ${options.name}: ${error.message}`);
throw error;
}
}
/**
* Start a service
*/
async startService(name: string): Promise<void> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
if (!service.containerID) {
throw new Error(`Service ${name} has no container ID`);
}
logger.info(`Starting service: ${name}`);
this.database.updateService(service.id!, { status: 'starting' });
await this.docker.startContainer(service.containerID);
this.database.updateService(service.id!, { status: 'running' });
logger.success(`Service started: ${name}`);
} catch (error) {
logger.error(`Failed to start service ${name}: ${error.message}`);
this.database.updateService(
this.database.getServiceByName(name)?.id!,
{ status: 'failed' }
);
throw error;
}
}
/**
* Stop a service
*/
async stopService(name: string): Promise<void> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
if (!service.containerID) {
throw new Error(`Service ${name} has no container ID`);
}
logger.info(`Stopping service: ${name}`);
this.database.updateService(service.id!, { status: 'stopping' });
await this.docker.stopContainer(service.containerID);
this.database.updateService(service.id!, { status: 'stopped' });
logger.success(`Service stopped: ${name}`);
} catch (error) {
logger.error(`Failed to stop service ${name}: ${error.message}`);
throw error;
}
}
/**
* Restart a service
*/
async restartService(name: string): Promise<void> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
if (!service.containerID) {
throw new Error(`Service ${name} has no container ID`);
}
logger.info(`Restarting service: ${name}`);
await this.docker.restartContainer(service.containerID);
this.database.updateService(service.id!, { status: 'running' });
logger.success(`Service restarted: ${name}`);
} catch (error) {
logger.error(`Failed to restart service ${name}: ${error.message}`);
throw error;
}
}
/**
* Remove a service (full cleanup)
*/
async removeService(name: string): Promise<void> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
logger.info(`Removing service: ${name}`);
// Stop and remove container
if (service.containerID) {
try {
await this.docker.removeContainer(service.containerID, true);
} catch (error) {
logger.warn(`Failed to remove container: ${error.message}`);
}
}
// Remove nginx config
if (service.domain) {
try {
await this.oneboxRef.nginx.removeConfig(service.id!);
await this.oneboxRef.nginx.reload();
} catch (error) {
logger.warn(`Failed to remove Nginx config: ${error.message}`);
}
// Note: We don't remove DNS records or SSL certs automatically
// as they might be used by other services or need manual cleanup
}
// Remove from database
this.database.deleteService(service.id!);
logger.success(`Service removed: ${name}`);
} catch (error) {
logger.error(`Failed to remove service ${name}: ${error.message}`);
throw error;
}
}
/**
* List all services
*/
listServices(): IService[] {
return this.database.getAllServices();
}
/**
* Get service by name
*/
getService(name: string): IService | null {
return this.database.getServiceByName(name);
}
/**
* Get service logs
*/
async getServiceLogs(name: string, tail = 100): Promise<string> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
if (!service.containerID) {
throw new Error(`Service ${name} has no container ID`);
}
const logs = await this.docker.getContainerLogs(service.containerID, tail);
return `=== STDOUT ===\n${logs.stdout}\n\n=== STDERR ===\n${logs.stderr}`;
} catch (error) {
logger.error(`Failed to get logs for service ${name}: ${error.message}`);
throw error;
}
}
/**
* Stream service logs (real-time)
*/
async streamServiceLogs(
name: string,
callback: (line: string, isError: boolean) => void
): Promise<void> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
if (!service.containerID) {
throw new Error(`Service ${name} has no container ID`);
}
await this.docker.streamContainerLogs(service.containerID, callback);
} catch (error) {
logger.error(`Failed to stream logs for service ${name}: ${error.message}`);
throw error;
}
}
/**
* Get service metrics
*/
async getServiceMetrics(name: string) {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
if (!service.containerID) {
throw new Error(`Service ${name} has no container ID`);
}
const stats = await this.docker.getContainerStats(service.containerID);
return stats;
} catch (error) {
logger.error(`Failed to get metrics for service ${name}: ${error.message}`);
return null;
}
}
/**
* Get service status
*/
async getServiceStatus(name: string): Promise<string> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
return 'not-found';
}
if (!service.containerID) {
return service.status;
}
const status = await this.docker.getContainerStatus(service.containerID);
return status;
} catch (error) {
logger.error(`Failed to get status for service ${name}: ${error.message}`);
return 'unknown';
}
}
/**
* Update service environment variables
*/
async updateServiceEnv(name: string, envVars: Record<string, string>): Promise<void> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
// Update database
this.database.updateService(service.id!, { envVars });
// Note: Requires container restart to take effect
logger.info(`Environment variables updated for ${name}. Restart service to apply changes.`);
} catch (error) {
logger.error(`Failed to update env vars for service ${name}: ${error.message}`);
throw error;
}
}
/**
* Sync service status from Docker
*/
async syncServiceStatus(name: string): Promise<void> {
try {
const service = this.database.getServiceByName(name);
if (!service || !service.containerID) {
return;
}
const status = await this.docker.getContainerStatus(service.containerID);
// Map Docker status to our status
let ourStatus: IService['status'] = 'stopped';
if (status === 'running') {
ourStatus = 'running';
} else if (status === 'exited' || status === 'dead') {
ourStatus = 'stopped';
} else if (status === 'created') {
ourStatus = 'stopped';
} else if (status === 'restarting') {
ourStatus = 'starting';
}
this.database.updateService(service.id!, { status: ourStatus });
} catch (error) {
logger.debug(`Failed to sync status for service ${name}: ${error.message}`);
}
}
/**
* Sync all service statuses from Docker
*/
async syncAllServiceStatuses(): Promise<void> {
const services = this.listServices();
for (const service of services) {
await this.syncServiceStatus(service.name);
}
}
}

317
ts/onebox.classes.ssl.ts Normal file
View File

@@ -0,0 +1,317 @@
/**
* SSL Manager for Onebox
*
* Manages SSL certificates via Let's Encrypt (using smartacme)
*/
import * as plugins from './onebox.plugins.ts';
import { logger } from './onebox.logging.ts';
import { OneboxDatabase } from './onebox.classes.database.ts';
export class OneboxSslManager {
private oneboxRef: any;
private database: OneboxDatabase;
private smartacme: plugins.smartacme.SmartAcme | null = null;
private acmeEmail: string | null = null;
constructor(oneboxRef: any) {
this.oneboxRef = oneboxRef;
this.database = oneboxRef.database;
}
/**
* Initialize SSL manager
*/
async init(): Promise<void> {
try {
// Get ACME email from settings
const acmeEmail = this.database.getSetting('acmeEmail');
if (!acmeEmail) {
logger.warn('ACME email not configured. SSL certificate management will be limited.');
logger.info('Configure with: onebox config set acmeEmail <email>');
return;
}
this.acmeEmail = acmeEmail;
// Initialize SmartACME
this.smartacme = new plugins.smartacme.SmartAcme({
email: acmeEmail,
environment: 'production', // or 'staging' for testing
dns: 'cloudflare', // Use Cloudflare DNS challenge
});
logger.info('SSL manager initialized with SmartACME');
} catch (error) {
logger.error(`Failed to initialize SSL manager: ${error.message}`);
throw error;
}
}
/**
* Check if SSL manager is configured
*/
isConfigured(): boolean {
return this.smartacme !== null && this.acmeEmail !== null;
}
/**
* Obtain SSL certificate for a domain
*/
async obtainCertificate(domain: string): Promise<void> {
try {
if (!this.isConfigured()) {
throw new Error('SSL manager not configured');
}
logger.info(`Obtaining SSL certificate for ${domain}...`);
// Check if certificate already exists and is valid
const existing = this.database.getSSLCertificate(domain);
if (existing && existing.expiryDate > Date.now()) {
logger.info(`Valid certificate already exists for ${domain}`);
return;
}
// Use certbot for now (smartacme integration would be more complex)
// This is a simplified version - in production, use proper ACME client
await this.obtainCertificateWithCertbot(domain);
// Store in database
const certPath = `/etc/letsencrypt/live/${domain}/cert.pem`;
const keyPath = `/etc/letsencrypt/live/${domain}/privkey.pem`;
const fullChainPath = `/etc/letsencrypt/live/${domain}/fullchain.pem`;
// Get expiry date (90 days from now for Let's Encrypt)
const expiryDate = Date.now() + 90 * 24 * 60 * 60 * 1000;
if (existing) {
this.database.updateSSLCertificate(domain, {
certPath,
keyPath,
fullChainPath,
expiryDate,
});
} else {
await this.database.createSSLCertificate({
domain,
certPath,
keyPath,
fullChainPath,
expiryDate,
issuer: 'Let\'s Encrypt',
createdAt: Date.now(),
updatedAt: Date.now(),
});
}
// Enable SSL in nginx config
await this.oneboxRef.nginx.enableSSL(domain);
logger.success(`SSL certificate obtained for ${domain}`);
} catch (error) {
logger.error(`Failed to obtain certificate for ${domain}: ${error.message}`);
throw error;
}
}
/**
* Obtain certificate using certbot
*/
private async obtainCertificateWithCertbot(domain: string): Promise<void> {
try {
logger.info(`Running certbot for ${domain}...`);
// Use webroot method (nginx serves .well-known/acme-challenge)
const command = new Deno.Command('certbot', {
args: [
'certonly',
'--webroot',
'--webroot-path=/var/www/certbot',
'--email',
this.acmeEmail!,
'--agree-tos',
'--no-eff-email',
'--domain',
domain,
'--non-interactive',
],
stdout: 'piped',
stderr: 'piped',
});
const { code, stderr } = await command.output();
if (code !== 0) {
const errorMsg = new TextDecoder().decode(stderr);
throw new Error(`Certbot failed: ${errorMsg}`);
}
logger.success(`Certbot obtained certificate for ${domain}`);
} catch (error) {
throw new Error(`Failed to run certbot: ${error.message}`);
}
}
/**
* Renew certificate for a domain
*/
async renewCertificate(domain: string): Promise<void> {
try {
logger.info(`Renewing SSL certificate for ${domain}...`);
const command = new Deno.Command('certbot', {
args: ['renew', '--cert-name', domain, '--non-interactive'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stderr } = await command.output();
if (code !== 0) {
const errorMsg = new TextDecoder().decode(stderr);
throw new Error(`Certbot renewal failed: ${errorMsg}`);
}
// Update database
const expiryDate = Date.now() + 90 * 24 * 60 * 60 * 1000;
this.database.updateSSLCertificate(domain, {
expiryDate,
});
logger.success(`Certificate renewed for ${domain}`);
// Reload nginx
await this.oneboxRef.nginx.reload();
} catch (error) {
logger.error(`Failed to renew certificate for ${domain}: ${error.message}`);
throw error;
}
}
/**
* List all certificates
*/
listCertificates() {
return this.database.getAllSSLCertificates();
}
/**
* Get certificate info for a domain
*/
getCertificate(domain: string) {
return this.database.getSSLCertificate(domain);
}
/**
* Check certificates that are expiring soon and renew them
*/
async renewExpiring(): Promise<void> {
try {
logger.info('Checking for expiring certificates...');
const certificates = this.listCertificates();
const thirtyDaysFromNow = Date.now() + 30 * 24 * 60 * 60 * 1000;
for (const cert of certificates) {
if (cert.expiryDate < thirtyDaysFromNow) {
logger.info(`Certificate for ${cert.domain} expires soon, renewing...`);
try {
await this.renewCertificate(cert.domain);
} catch (error) {
logger.error(`Failed to renew ${cert.domain}: ${error.message}`);
// Continue with other certificates
}
}
}
logger.success('Certificate renewal check complete');
} catch (error) {
logger.error(`Failed to check expiring certificates: ${error.message}`);
throw error;
}
}
/**
* Force renewal of all certificates
*/
async renewAll(): Promise<void> {
try {
logger.info('Renewing all certificates...');
const command = new Deno.Command('certbot', {
args: ['renew', '--force-renewal', '--non-interactive'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stderr } = await command.output();
if (code !== 0) {
const errorMsg = new TextDecoder().decode(stderr);
throw new Error(`Certbot renewal failed: ${errorMsg}`);
}
logger.success('All certificates renewed');
// Reload nginx
await this.oneboxRef.nginx.reload();
} catch (error) {
logger.error(`Failed to renew all certificates: ${error.message}`);
throw error;
}
}
/**
* Check if certbot is installed
*/
async isCertbotInstalled(): Promise<boolean> {
try {
const command = new Deno.Command('which', {
args: ['certbot'],
stdout: 'piped',
stderr: 'piped',
});
const { code } = await command.output();
return code === 0;
} catch {
return false;
}
}
/**
* Get certificate expiry date from file
*/
async getCertificateExpiry(domain: string): Promise<Date | null> {
try {
const certPath = `/etc/letsencrypt/live/${domain}/cert.pem`;
const command = new Deno.Command('openssl', {
args: ['x509', '-enddate', '-noout', '-in', certPath],
stdout: 'piped',
stderr: 'piped',
});
const { code, stdout } = await command.output();
if (code !== 0) {
return null;
}
const output = new TextDecoder().decode(stdout);
const match = output.match(/notAfter=(.+)/);
if (match) {
return new Date(match[1]);
}
return null;
} catch (error) {
logger.error(`Failed to get certificate expiry for ${domain}: ${error.message}`);
return null;
}
}
}

366
ts/onebox.cli.ts Normal file
View File

@@ -0,0 +1,366 @@
/**
* CLI Router for Onebox
*/
import { logger } from './onebox.logging.ts';
import { projectInfo } from './onebox.info.ts';
import { Onebox } from './onebox.classes.onebox.ts';
export async function runCli(): Promise<void> {
const args = Deno.args;
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
printHelp();
return;
}
if (args.includes('--version') || args.includes('-v')) {
console.log(`${projectInfo.name} v${projectInfo.version}`);
return;
}
const command = args[0];
const subcommand = args[1];
try {
// Initialize Onebox
const onebox = new Onebox();
await onebox.init();
// Route commands
switch (command) {
case 'service':
await handleServiceCommand(onebox, subcommand, args.slice(2));
break;
case 'registry':
await handleRegistryCommand(onebox, subcommand, args.slice(2));
break;
case 'dns':
await handleDnsCommand(onebox, subcommand, args.slice(2));
break;
case 'ssl':
await handleSslCommand(onebox, subcommand, args.slice(2));
break;
case 'nginx':
await handleNginxCommand(onebox, subcommand, args.slice(2));
break;
case 'daemon':
await handleDaemonCommand(onebox, subcommand, args.slice(2));
break;
case 'config':
await handleConfigCommand(onebox, subcommand, args.slice(2));
break;
case 'status':
await handleStatusCommand(onebox);
break;
default:
logger.error(`Unknown command: ${command}`);
printHelp();
Deno.exit(1);
}
// Cleanup
await onebox.shutdown();
} catch (error) {
logger.error(error.message);
Deno.exit(1);
}
}
// Service commands
async function handleServiceCommand(onebox: Onebox, subcommand: string, args: string[]) {
switch (subcommand) {
case 'add': {
const name = args[0];
const image = getArg(args, '--image');
const domain = getArg(args, '--domain');
const port = parseInt(getArg(args, '--port') || '80', 10);
const envArgs = args.filter((a) => a.startsWith('--env=')).map((a) => a.slice(6));
const envVars: Record<string, string> = {};
for (const env of envArgs) {
const [key, value] = env.split('=');
envVars[key] = value;
}
await onebox.services.deployService({ name, image, port, domain, envVars });
break;
}
case 'remove':
await onebox.services.removeService(args[0]);
break;
case 'start':
await onebox.services.startService(args[0]);
break;
case 'stop':
await onebox.services.stopService(args[0]);
break;
case 'restart':
await onebox.services.restartService(args[0]);
break;
case 'list': {
const services = onebox.services.listServices();
logger.table(
['Name', 'Image', 'Status', 'Domain', 'Port'],
services.map((s) => [s.name, s.image, s.status, s.domain || '-', s.port.toString()])
);
break;
}
case 'logs': {
const logs = await onebox.services.getServiceLogs(args[0]);
console.log(logs);
break;
}
default:
logger.error(`Unknown service subcommand: ${subcommand}`);
}
}
// Registry commands
async function handleRegistryCommand(onebox: Onebox, subcommand: string, args: string[]) {
switch (subcommand) {
case 'add': {
const url = getArg(args, '--url');
const username = getArg(args, '--username');
const password = getArg(args, '--password');
await onebox.registries.addRegistry(url, username, password);
break;
}
case 'remove':
await onebox.registries.removeRegistry(getArg(args, '--url'));
break;
case 'list': {
const registries = onebox.registries.listRegistries();
logger.table(
['URL', 'Username'],
registries.map((r) => [r.url, r.username])
);
break;
}
default:
logger.error(`Unknown registry subcommand: ${subcommand}`);
}
}
// DNS commands
async function handleDnsCommand(onebox: Onebox, subcommand: string, args: string[]) {
switch (subcommand) {
case 'add':
await onebox.dns.addDNSRecord(args[0]);
break;
case 'remove':
await onebox.dns.removeDNSRecord(args[0]);
break;
case 'list': {
const records = onebox.dns.listDNSRecords();
logger.table(
['Domain', 'Type', 'Value'],
records.map((r) => [r.domain, r.type, r.value])
);
break;
}
case 'sync':
await onebox.dns.syncFromCloudflare();
break;
default:
logger.error(`Unknown dns subcommand: ${subcommand}`);
}
}
// SSL commands
async function handleSslCommand(onebox: Onebox, subcommand: string, args: string[]) {
switch (subcommand) {
case 'renew':
if (args[0]) {
await onebox.ssl.renewCertificate(args[0]);
} else {
await onebox.ssl.renewExpiring();
}
break;
case 'list': {
const certs = onebox.ssl.listCertificates();
logger.table(
['Domain', 'Expiry', 'Issuer'],
certs.map((c) => [c.domain, new Date(c.expiryDate).toISOString(), c.issuer])
);
break;
}
case 'force-renew':
await onebox.ssl.renewCertificate(args[0]);
break;
default:
logger.error(`Unknown ssl subcommand: ${subcommand}`);
}
}
// Nginx commands
async function handleNginxCommand(onebox: Onebox, subcommand: string, _args: string[]) {
switch (subcommand) {
case 'reload':
await onebox.nginx.reload();
break;
case 'test':
await onebox.nginx.test();
break;
case 'status': {
const status = await onebox.nginx.getStatus();
logger.info(`Nginx status: ${status}`);
break;
}
default:
logger.error(`Unknown nginx subcommand: ${subcommand}`);
}
}
// Daemon commands
async function handleDaemonCommand(onebox: Onebox, subcommand: string, _args: string[]) {
switch (subcommand) {
case 'install':
await onebox.daemon.installService();
break;
case 'start':
await onebox.startDaemon();
break;
case 'stop':
await onebox.stopDaemon();
break;
case 'logs': {
const command = new Deno.Command('journalctl', {
args: ['-u', 'smartdaemon_onebox', '-f'],
stdout: 'inherit',
stderr: 'inherit',
});
await command.output();
break;
}
case 'status': {
const status = await onebox.daemon.getServiceStatus();
logger.info(`Daemon status: ${status}`);
break;
}
default:
logger.error(`Unknown daemon subcommand: ${subcommand}`);
}
}
// Config commands
async function handleConfigCommand(onebox: Onebox, subcommand: string, args: string[]) {
switch (subcommand) {
case 'show': {
const settings = onebox.database.getAllSettings();
logger.table(
['Key', 'Value'],
Object.entries(settings).map(([k, v]) => [k, v])
);
break;
}
case 'set':
onebox.database.setSetting(args[0], args[1]);
logger.success(`Setting ${args[0]} updated`);
break;
default:
logger.error(`Unknown config subcommand: ${subcommand}`);
}
}
// Status command
async function handleStatusCommand(onebox: Onebox) {
const status = await onebox.getSystemStatus();
console.log(JSON.stringify(status, null, 2));
}
// Helpers
function getArg(args: string[], flag: string): string {
const arg = args.find((a) => a.startsWith(`${flag}=`));
return arg ? arg.split('=')[1] : '';
}
function printHelp(): void {
console.log(`
Onebox v${projectInfo.version} - Self-hosted container platform
Usage: onebox <command> [options]
Commands:
service add <name> --image <image> [--domain <domain>] [--port <port>] [--env KEY=VALUE]
service remove <name>
service start <name>
service stop <name>
service restart <name>
service list
service logs <name>
registry add --url <url> --username <user> --password <pass>
registry remove --url <url>
registry list
dns add <domain>
dns remove <domain>
dns list
dns sync
ssl renew [domain]
ssl list
ssl force-renew <domain>
nginx reload
nginx test
nginx status
daemon install
daemon start
daemon stop
daemon logs
daemon status
config show
config set <key> <value>
status
Options:
--help, -h Show this help message
--version, -v Show version
--debug Enable debug logging
Examples:
onebox service add myapp --image nginx:latest --domain app.example.com --port 80
onebox registry add --url registry.example.com --username user --password pass
onebox daemon install
onebox daemon start
`);
}

12
ts/onebox.info.ts Normal file
View File

@@ -0,0 +1,12 @@
/**
* Project information and version
*/
import denoConfig from '../deno.json' with { type: 'json' };
export const projectInfo = {
name: denoConfig.name,
version: denoConfig.version,
description: 'Self-hosted container platform with automatic SSL and DNS',
repository: 'https://code.foss.global/serve.zone/onebox',
};

124
ts/onebox.logging.ts Normal file
View File

@@ -0,0 +1,124 @@
/**
* Logging utilities for Onebox
*/
type LogLevel = 'info' | 'success' | 'warn' | 'error' | 'debug';
class Logger {
private debugMode = false;
constructor() {
this.debugMode = Deno.args.includes('--debug') || Deno.env.get('DEBUG') === 'true';
}
/**
* Log a message with specified level
*/
log(level: LogLevel, message: string, ...args: unknown[]): void {
const timestamp = new Date().toISOString();
const prefix = this.getPrefix(level);
const formattedMessage = `${prefix} ${message}`;
switch (level) {
case 'error':
console.error(formattedMessage, ...args);
break;
case 'warn':
console.warn(formattedMessage, ...args);
break;
case 'debug':
if (this.debugMode) {
console.log(formattedMessage, ...args);
}
break;
default:
console.log(formattedMessage, ...args);
}
}
/**
* Info level logging
*/
info(message: string, ...args: unknown[]): void {
this.log('info', message, ...args);
}
/**
* Success level logging
*/
success(message: string, ...args: unknown[]): void {
this.log('success', message, ...args);
}
/**
* Warning level logging
*/
warn(message: string, ...args: unknown[]): void {
this.log('warn', message, ...args);
}
/**
* Error level logging
*/
error(message: string, ...args: unknown[]): void {
this.log('error', message, ...args);
}
/**
* Debug level logging (only when --debug flag is present)
*/
debug(message: string, ...args: unknown[]): void {
this.log('debug', message, ...args);
}
/**
* Get colored prefix for log level
*/
private getPrefix(level: LogLevel): string {
const colors = {
info: '\x1b[36m', // Cyan
success: '\x1b[32m', // Green
warn: '\x1b[33m', // Yellow
error: '\x1b[31m', // Red
debug: '\x1b[90m', // Gray
};
const reset = '\x1b[0m';
const icons = {
info: '',
success: '✓',
warn: '⚠',
error: '✖',
debug: '⚙',
};
return `${colors[level]}${icons[level]}${reset}`;
}
/**
* Print a table (simplified version)
*/
table(headers: string[], rows: string[][]): void {
// Calculate column widths
const widths = headers.map((header, i) => {
const maxContentWidth = Math.max(
...rows.map((row) => (row[i] || '').toString().length)
);
return Math.max(header.length, maxContentWidth);
});
// Print header
const headerRow = headers.map((h, i) => h.padEnd(widths[i])).join(' ');
console.log(headerRow);
console.log(headers.map((_, i) => '-'.repeat(widths[i])).join(' '));
// Print rows
for (const row of rows) {
const formattedRow = row.map((cell, i) => (cell || '').toString().padEnd(widths[i])).join(' ');
console.log(formattedRow);
}
}
}
export const logger = new Logger();

46
ts/onebox.plugins.ts Normal file
View File

@@ -0,0 +1,46 @@
/**
* Centralized dependency imports for Onebox
*
* This file serves as the single source of truth for all external dependencies.
* All modules should import from this file using: import * as plugins from './onebox.plugins.ts'
*/
// Deno Standard Library
import * as path from '@std/path';
import * as fs from '@std/fs';
import * as http from '@std/http';
import * as encoding from '@std/encoding';
export { path, fs, http, encoding };
// Database
import * as sqlite from '@db/sqlite';
export { sqlite };
// Systemd Daemon Integration
import * as smartdaemon from '@push.rocks/smartdaemon';
export { smartdaemon };
// Docker API Client
import * as docker from '@apiclient.xyz/docker';
export { docker };
// Cloudflare DNS Management
import * as cloudflare from '@apiclient.xyz/cloudflare';
export { cloudflare };
// Let's Encrypt / ACME
import * as smartacme from '@push.rocks/smartacme';
export { smartacme };
// Crypto utilities (for password hashing, encryption)
import * as bcrypt from 'https://deno.land/x/bcrypt@v0.4.1/mod.ts';
export { bcrypt };
// JWT for authentication
import { create as createJwt, verify as verifyJwt, decode as decodeJwt } from 'https://deno.land/x/djwt@v3.0.2/mod.ts';
export { createJwt, verifyJwt, decodeJwt };
// Crypto key management
import { crypto } from 'https://deno.land/std@0.208.0/crypto/mod.ts';
export { crypto };

165
ts/onebox.types.ts Normal file
View File

@@ -0,0 +1,165 @@
/**
* Type definitions for Onebox
*/
// Service types
export interface IService {
id?: number;
name: string;
image: string;
registry?: string;
envVars: Record<string, string>;
port: number;
domain?: string;
containerID?: string;
status: 'stopped' | 'starting' | 'running' | 'stopping' | 'failed';
createdAt: number;
updatedAt: number;
}
// Registry types
export interface IRegistry {
id?: number;
url: string;
username: string;
passwordEncrypted: string;
createdAt: number;
}
// Nginx configuration types
export interface INginxConfig {
id?: number;
serviceId: number;
domain: string;
port: number;
sslEnabled: boolean;
configTemplate: string;
createdAt: number;
updatedAt: number;
}
// SSL certificate types
export interface ISslCertificate {
id?: number;
domain: string;
certPath: string;
keyPath: string;
fullChainPath: string;
expiryDate: number;
issuer: string;
createdAt: number;
updatedAt: number;
}
// DNS record types
export interface IDnsRecord {
id?: number;
domain: string;
type: 'A' | 'AAAA' | 'CNAME';
value: string;
cloudflareID?: string;
createdAt: number;
updatedAt: number;
}
// Metrics types
export interface IMetric {
id?: number;
serviceId: number;
timestamp: number;
cpuPercent: number;
memoryUsed: number;
memoryLimit: number;
networkRxBytes: number;
networkTxBytes: number;
}
// Log entry types
export interface ILogEntry {
id?: number;
serviceId: number;
timestamp: number;
message: string;
level: 'info' | 'warn' | 'error' | 'debug';
source: 'stdout' | 'stderr';
}
// User types
export interface IUser {
id?: number;
username: string;
passwordHash: string;
role: 'admin' | 'user';
createdAt: number;
updatedAt: number;
}
// Settings types
export interface ISetting {
key: string;
value: string;
updatedAt: number;
}
// Application settings
export interface IAppSettings {
serverIP?: string;
cloudflareAPIKey?: string;
cloudflareEmail?: string;
cloudflareZoneID?: string;
acmeEmail?: string;
nginxConfigDir?: string;
dataDir?: string;
httpPort?: number;
metricsInterval?: number;
logRetentionDays?: number;
}
// Container stats from Docker
export interface IContainerStats {
cpuPercent: number;
memoryUsed: number;
memoryLimit: number;
memoryPercent: number;
networkRx: number;
networkTx: number;
}
// Service deployment options
export interface IServiceDeployOptions {
name: string;
image: string;
registry?: string;
envVars?: Record<string, string>;
port: number;
domain?: string;
autoSSL?: boolean;
autoDNS?: boolean;
}
// HTTP API request/response types
export interface IApiResponse<T = unknown> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
export interface ILoginRequest {
username: string;
password: string;
}
export interface ILoginResponse {
token: string;
user: {
username: string;
role: string;
};
}
// CLI command types
export interface ICliArgs {
_: string[];
[key: string]: unknown;
}