Part of the woly-server monorepo. Per-LAN Wake-on-LAN agent with automatic network discovery.
- Automatic network discovery via ARP scanning with DNS/NetBIOS hostname resolution
- Wake-on-LAN magic packet sending
- Dual status tracking —
status(ARP-based, reliable) andpingResponsive(ICMP, diagnostic) - Standalone or agent mode (connects to C&C backend via WebSocket)
- Rate limiting, input validation, CORS, Helmet security headers
- Interactive Swagger API docs at
/api-docs - Structured Winston logging with file rotation
- 240+ tests with 80%+ coverage
- Node.js 24+ (see root
.nvmrc) - npm 10+
npm install
cp apps/node-agent/.env.example apps/node-agent/.env
npm run dev:node-agentcd apps/node-agent
npm run dev240+ tests with enforced coverage thresholds (50% branches/functions/lines/statements).
npm test # All tests
npm run test:coverage # With coverage report
npm run test:watch # Watch mode
npm run test:unit # Unit tests only
npm run test:integration # Integration tests only
npm run test:ci # CI mode
npm run typecheck # Type-check without emittingTest organization:
- Unit tests:
src/**/__tests__/*.unit.test.ts(alongside source) - Integration tests:
src/__tests__/*.integration.test.ts
Hosts have two separate status indicators:
- Source: ARP network discovery
- Meaning: If a device responds to ARP, it's on the network and awake
- Reliability: Very reliable - ARP responses mean the device is active
- Source: ICMP ping test
- Values:
1- Host responds to ping0- Host doesn't respond to ping (may still be awake due to firewall)null- Not yet tested
- Meaning: Additional diagnostic information about network reachability
- Note: Many devices block ping for security, so
pingResponsive: 0doesn't mean the host is asleep
Recommended interpretation: Use status for determining if a device is awake. Use pingResponsive for network diagnostics and troubleshooting.
Dependency/security tracking notes and mitigation strategy are documented in SECURITY.md.
Visit http://localhost:8082/api-docs for interactive Swagger UI documentation with:
- Complete endpoint descriptions
- Request/response schemas
- Try-it-out functionality
- Example payloads
GET /healthResponse:
{
"uptime": 73.876685,
"timestamp": 1763544894939,
"status": "ok",
"environment": "development",
"build": {
"version": "0.0.1",
"protocolVersion": "1.0.0"
},
"agent": {
"mode": "agent",
"authMode": "session-token",
"connected": true
},
"checks": {
"database": "healthy",
"networkScan": "idle"
},
"telemetry": {
"reconnect": {
"scheduled": 0,
"failed": 0
},
"auth": {
"expired": 0,
"revoked": 0,
"unavailable": 0
},
"protocol": {
"inboundValidationFailures": 0,
"outboundValidationFailures": 0,
"unsupported": 0,
"errors": 0
}
}
}GET /hostsResponse:
{
"hosts": [
{
"name": "PHANTOM-MBP",
"mac": "80:6D:97:60:39:08",
"ip": "192.168.1.147",
"status": "awake",
"lastSeen": "2025-11-19 09:24:30",
"discovered": 1,
"pingResponsive": 1
}
],
"scanInProgress": false,
"lastScanTime": "2025-11-19T09:24:30.000Z"
}GET /hosts/:nameExample:
curl http://localhost:8082/hosts/PHANTOM-MBPPOST /hosts/wakeup/:nameExample:
curl -X POST http://localhost:8082/hosts/wakeup/PHANTOM-MBPResponse:
{
"success": true,
"name": "PHANTOM-MBP",
"mac": "80:6D:97:60:39:08",
"message": "Wake-on-LAN packet sent"
}POST /hosts/scanRate Limited: 5 requests per minute
Example:
curl -X POST http://localhost:8082/hosts/scanPOST /hosts
Content-Type: application/json
{
"name": "MY-DEVICE",
"mac": "AA:BB:CC:DD:EE:FF",
"ip": "192.168.1.100"
}Example:
curl -X POST http://localhost:8082/hosts \
-H "Content-Type: application/json" \
-d '{"name":"MY-DEVICE","mac":"AA:BB:CC:DD:EE:FF","ip":"192.168.1.100"}'GET /hosts/mac-vendor/:macExample:
curl http://localhost:8082/hosts/mac-vendor/80:6D:97:60:39:08Response:
{
"mac": "80:6D:97:60:39:08",
"vendor": "Apple, Inc.",
"source": "macvendors.com (cached)"
}Configuration is managed via environment variables. Create a .env file:
# Server Configuration
PORT=8082
HOST=0.0.0.0
NODE_ENV=development
# Database
DB_PATH=./db/woly.db
# Network Discovery
SCAN_INTERVAL=300000 # 5 minutes
SCAN_DELAY=5000 # 5 seconds initial delay
PING_TIMEOUT=2000 # 2 seconds
USE_PING_VALIDATION=false # Use ping to validate awake status (default: false, ARP is sufficient)
# Note: ARP discovery means a host is responding on the network (awake)
# Ping validation is optional but may fail even for awake hosts due to firewalls
# All hosts are always ping-tested to track pingResponsive status (separate from awake/asleep)
# Caching
MAC_VENDOR_TTL=86400000 # 24 hours
MAC_VENDOR_RATE_LIMIT=1000 # 1 second between API calls
# CORS
CORS_ORIGINS=http://localhost:19000,http://192.168.1.228:8082
# Logging
LOG_LEVEL=info # error, warn, info, http, debug
# Agent mode + tunnel dispatch
NODE_MODE=agent
CNC_URL=wss://cnc.example.com
NODE_ID=home-office-node
NODE_LOCATION=Home Office
NODE_AUTH_TOKEN=replace-with-cnc-node-token
TUNNEL_MODE=direct # "direct" or "cloudflare"
CLOUDFLARE_TUNNEL_URL= # required when TUNNEL_MODE=cloudflare
CLOUDFLARE_TUNNEL_TOKEN=# required when TUNNEL_MODE=cloudflare
NODE_PUBLIC_URL= # optional override for non-cloudflare public URLsUse this when you want C&C command routing through a Cloudflare public endpoint for the node-agent.
- Create/authenticate a Cloudflare Tunnel for the node-agent service (
http://localhost:8082). - Configure node-agent:
NODE_MODE=agent
TUNNEL_MODE=cloudflare
CLOUDFLARE_TUNNEL_URL=https://<your-tunnel-hostname>
CLOUDFLARE_TUNNEL_TOKEN=<your-cloudflare-tunnel-token>- Keep
NODE_AUTH_TOKENaligned with C&CNODE_AUTH_TOKENS. - Start
cloudflaredwith your tunnel token. - Start node-agent and verify registration in C&C (
GET /api/nodesshould showpublicUrl).
When tunnel dispatch fails or is unavailable, C&C falls back to the direct WebSocket path automatically.
NEW: Optional API key authentication for /hosts/* endpoints.
- Enable: Set
NODE_API_KEYenvironment variable - Disable: Leave
NODE_API_KEYunset (default - standalone mode) - Header Format:
Authorization: Bearer <your-api-key> - Protected Endpoints: All
/hosts/*routes when enabled - Public Endpoints:
/health(always accessible)
Example Usage:
# Request without authentication (fails when NODE_API_KEY is set)
curl http://localhost:8082/hosts
# Response: 401 Unauthorized
# Request with authentication
curl -H "Authorization: Bearer your-api-key" http://localhost:8082/hosts
# Response: 200 OK with hosts list
# Health check (always public)
curl http://localhost:8082/health
# Response: 200 OK (no auth required)Security Features:
- Constant-time key comparison (prevents timing attacks)
- Flexible whitespace handling per HTTP spec
- Case-sensitive validation
- Descriptive error messages
Recommendation: Enable for deployments exposed beyond local network.
- General API: 100 requests per 15 minutes per IP
- Network Scans: 5 requests per minute per IP
- Wake Requests: 20 requests per minute per IP
Rate limit information is returned in response headers:
RateLimit-Limit: Maximum requests per windowRateLimit-Remaining: Requests remainingRateLimit-Reset: Seconds until reset
All endpoints validate input using Joi schemas:
- MAC address format:
XX:XX:XX:XX:XX:XXorXX-XX-XX-XX-XX-XX - IP address format: Valid IPv4/IPv6
- Hostname: 1-255 characters
Helmet.js provides security headers:
- Content Security Policy
- X-Frame-Options
- X-Content-Type-Options
- Referrer-Policy
Configurable CORS origins via environment variable.
All errors follow a standardized format:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "MAC address must be in format XX:XX:XX:XX:XX:XX",
"statusCode": 400,
"timestamp": "2025-11-19T09:35:06.541Z",
"path": "/hosts/mac-vendor/INVALID"
}
}VALIDATION_ERROR(400) - Invalid inputNOT_FOUND(404) - Resource not foundINTERNAL_ERROR(500) - Server error
docker build -t woly-backend:latest .docker run -d \
--name woly-backend \
--net host \
-v $(pwd)/db:/app/db \
-v $(pwd)/logs:/app/logs \
-e NODE_ENV=production \
woly-backend:latestdocker-compose up -dNote: Host networking mode is required for ARP scanning.
Winston-based structured logging with levels:
error: Error messages (logged tologs/error.log)warn: Warningsinfo: General informationhttp: HTTP requestsdebug: Detailed debugging
Logs are written to:
- Console (colored output in development)
logs/combined.log(all levels)logs/error.log(errors only)
apps/node-agent/
├── src/
│ ├── app.ts # Express app + initialization
│ ├── types.ts # Local TypeScript types
│ ├── swagger.ts # OpenAPI/Swagger config
│ ├── config/ # Environment configuration
│ ├── controllers/ # Request handlers
│ ├── middleware/ # Error handling, rate limiting, validation
│ ├── routes/ # Route definitions
│ ├── services/ # Business logic (hostDatabase, networkDiscovery, agent)
│ ├── utils/ # Logger
│ └── validators/ # Joi schemas
├── types/ # Ambient .d.ts for untyped packages
├── jest.config.js
├── tsconfig.json # Extends ../../tsconfig.base.json
└── Dockerfile
- Compatibility Matrix — Node ↔ C&C version compatibility
- Architecture Plan — Multi-phase evolution roadmap
- Implementation Checklist — Progress tracking
- Security — Security considerations and practices
- Dependency Remediation Plan — Dependency audit policy and exceptions
- Testing — Testing strategy and guidelines
- Runbook: Reconnect/Auth Loop
- Runbook: Protocol Validation Failures
- Rollout Policy: Canary to Staged
- ADR 0001: Node Auth Token Transport
- ADR 0002: Shared Protocol Package
- ADR 0003: Command Reliability and Idempotency
Apache License 2.0 (see LICENSE in the repo root).