Create MeshCentral workspace

This commit is contained in:
2026-07-11 10:44:42 -05:00
commit 37a8586d34
34 changed files with 6930 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
# meshpress-bridge Changelog
## Current
- Fixed agent-link generation to pass the resolved group object instead of a boolean match flag.
- Added selective WordPress event pushes for high-signal bridge lifecycle events so MeshPress can forward them into OmniLog.
- Added bridge status visibility for WordPress event push configuration.
## 0.4.3 - 2026-06-12
- Fixed agent-link generation to pass the resolved group object instead of a boolean match flag.
- Added selective WordPress event pushes for high-signal bridge lifecycle events so MeshPress can forward them into OmniLog.
- Added bridge status visibility for WordPress event push configuration.
## 0.4.2 - 2026-05-25
- Added redacted MeshCentral config auditing for MySQL/MariaDB, framing, logging, backup, and maintenance posture.
- Added real MeshCentral `settings.maintenanceMode` toggling with config backup and service restart support.
- Replaced the data-only backup with a guarded full snapshot that includes MeshCentral files, config, manifest, checksum, and a MySQL/MariaDB dump when configured.
- Added a configurable database dump timeout (`MESHPRESS_DB_DUMP_TIMEOUT_SECONDS`) so backup failures produce clear diagnostics instead of hanging.
## 0.4.1 - 2026-05-25
- Resolved MeshCentral groups before generating agent links, so WordPress punctuation drift no longer breaks installer commands.
- Generated Linux install commands from the canonical group short id, including MeshCentral `@` characters.
- Returned the matched group record and group name with agent-link responses for clearer MeshPress output.
- Returned normalized group `short_id` values so agent-link generation can preserve MeshCentral punctuation across the full path.
- Added installer-shape diagnostics and an explicit warning when the resolved short Mesh ID looks too short.
- Added a curl fallback command while keeping the native MeshCentral wget installer command as the primary Linux path.
## 0.4.0 - 2026-05-24
- Resolved MeshCentral groups before generating agent links, so WordPress punctuation drift no longer breaks installer commands.
- Generated Linux install commands from the canonical group short id, including MeshCentral `@` characters.
- Returned the matched group record and group name with agent-link responses for clearer MeshPress output.
## 0.3.9 - 2026-05-24
- Replaced the bridge updater with a staged, verified, rollback-capable update flow that does not delete PM2 before the new copy is ready.
- Added catalog update checks and a runtime maintenance-mode toggle endpoint for MeshPress.
- Made group-id resolution tolerant of MeshCentral punctuation drift, and switched Linux agent commands to MeshCentral's native install-script pattern.
## 0.3.8 - 2026-05-24
- Group edits now verify the refreshed MeshCentral group record before returning success.
- Added extra group edit diagnostics when MeshCtrl accepts a command but the group does not actually change.
- Tried both `--desc` and `--description` edit forms for better MeshCtrl version compatibility.
## 0.3.7 - 2026-05-24
- Added guarded MeshCentral maintenance endpoints for version/update checks, data backups, and update triggering.
- Added agent uninstall diagnostics and richer copy-friendly bridge logging for lifecycle testing.
- Hardened the bridge installer/updater so running from the live `/opt` target refuses instead of deleting itself.
## 0.3.6 - 2026-05-24
- Rejected MeshCtrl failure text even when MeshCtrl exits with code 0, preventing false-success group edit responses.
- Added richer group edit candidate attempts and copyable JSON diagnostics for invalid group identifiers.
- Expanded agent link generation with Windows, Linux, macOS, Mesh Assistant, and Linux install command output.
## 0.3.5 - 2026-05-24
- Changed MeshCentral session links to prefer the working `viewmode-first + gotonode` URL pattern.
- Added short-node-id extraction for `gotonode` links so full `node//...` ids do not break direct console sessions.
- Added `/api/session-diagnostics/:nodeId` plus guarded device rename and move endpoints for manual MeshPress testing.
## 0.3.4 - 2026-05-23
- Added bridge runtime version reporting and a guarded manual update endpoint for MeshPress admin.
- Added richer MeshCentral session-link generation for Desktop, Console, and Files with multiple direct-link candidates.
- Added `console` and `shell` aliases for MeshCentral terminal sessions.
## 0.3.3 - 2026-05-23
- Added `/api/version`, `/api/update-now`, and `/api/session-links/:nodeId`.
- Added all-link candidate generation for MeshCentral session testing.
- Restarted bridge updates with `pm2 --update-env` so env/config changes are picked up.
## 0.3.2 - 2026-05-23
- Hardened bridge authentication for public-port exposure by requiring a configured bearer token on all endpoints.
- Added optional exact-IP allowlisting and failed-auth rate limiting.
- Moved WordPress fleet push authentication from query string to Authorization header.
- Added startup security warnings for all-interface binding without app-layer allowlisting.
## 0.3.1 - 2026-05-23
- Added bridge self-test, server-info, users, events, and device-info diagnostics for MeshPress.
- Added configurable MeshCentral direct-link style with `MESHPRESS_LINK_MODE`.
- Added guarded provisioning endpoints for group edit/remove, group user assignment, invite links, and simple device message/toast commands.
- Added cache clearing and richer status output for faster troubleshooting.
## 0.3.0 - 2026-05-22
- Added Phase 5.2 MeshCentral provisioning endpoints for Service Desk.
- Added authenticated agent/provisioning link generation for MeshCentral device groups.
- Added guarded device group creation through MeshCtrl `adddevicegroup`.
- Required `MESHPRESS_ALLOW_COMMANDS=1` before the bridge will create MeshCentral groups, keeping provisioning actions opt-in.
- Returned Windows x64, x86, ARM64, and interactive Windows agent links for linked MeshCentral groups.
<!-- GP-RECONSTRUCTED-CHANGELOG:START -->
## Reconstructed History
These entries were inferred from local distribution archives, module headers, and `.old` backups. They are intentionally conservative and may not capture the full intent of each change.
### 0.2.0 - reconstructed
- Reconstructed from archive `meshpress-bridge-v0.2.0.zip` (2026-05-20).
- Header description at this version: GracePress MeshPress Bridge. Pushes MeshCentral fleet snapshots to WordPress MeshPress.
- Catalog/category marker: Hidden.
### 0.1.1 - reconstructed
- Reconstructed from 2 file backup(s) in `tools/meshpress-bridge/.old`.
- Exact intent unknown; this confirms a recoverable historical version existed.
### 0.1.0 - reconstructed
- Reconstructed from 1 file backup(s) in `tools/meshpress-bridge/.old`.
- Exact intent unknown; this confirms a recoverable historical version existed.
### VERSION, - reconstructed
- Reconstructed from 2 file backup(s) in `tools/meshpress-bridge/.old`.
- Exact intent unknown; this confirms a recoverable historical version existed.
<!-- GP-RECONSTRUCTED-CHANGELOG:END -->
+184
View File
@@ -0,0 +1,184 @@
# GracePress MeshPress Bridge
Small local HTTP bridge between WordPress/MeshPress and MeshCentral.
The bridge runs beside MeshCentral and uses MeshCentral's official `meshctrl` tool to read device groups and devices. WordPress talks to this bridge instead of trying to speak MeshCentral websocket protocol directly.
## Endpoints
- `GET /api/health`
- `GET /api/status`
- `GET /api/self-test`
- `GET /api/groups`
- `GET /api/users`
- `GET /api/events?limit=50`
- `GET /api/serverinfo`
- `GET /api/devices`
- `GET /api/device/:nodeId`
- `GET /api/fleet`
- `POST /api/push-now`
- `POST /api/cache/clear`
- `GET /api/link/desktop/:nodeId`
- `GET /api/link/terminal/:nodeId`
- `GET /api/link/files/:nodeId`
- `GET /api/link/:mode/:nodeId?link_mode=both`
- `GET /api/agent-links/:groupId`
- `POST /api/groups/create`
- `POST /api/groups/edit`
- `POST /api/groups/remove`
- `POST /api/groups/add-user`
- `POST /api/groups/remove-user`
- `POST /api/invite-link`
- `POST /api/device/message`
- `POST /api/device/toast`
- `POST /api/action`
All endpoints require `Authorization: Bearer TOKEN`. If `MESHPRESS_BRIDGE_TOKEN` is empty, protected requests are rejected.
`/api/action` is intentionally locked down for now. It returns a clear unsupported-command response until each broad remote-command workflow is whitelisted and tested.
Provisioning and device-command endpoints require:
```bash
MESHPRESS_ALLOW_COMMANDS="1"
```
Without that flag the bridge will read MeshCentral state but refuse provisioning actions.
## Install
```bash
cd /storage/meshcentral-workspace/bridge/meshpress-bridge
sudo ./install.sh
sudo nano /etc/gracepress/meshpress-bridge.env
pm2 restart gp-meshpress-bridge
```
## Required Config
```bash
MESHPRESS_BRIDGE_PORT="3099"
MESHPRESS_BRIDGE_BIND="127.0.0.1"
MESHPRESS_BRIDGE_TOKEN="generated-secret"
MESHPRESS_ALLOWED_IPS=""
MESHPRESS_AUTH_WINDOW_SECONDS="900"
MESHPRESS_AUTH_MAX_FAILURES="30"
MESHCTRL_URL="wss://127.0.0.1"
PUBLIC_MESH_URL="https://mesh.yourdomain.com"
MESHCTRL_USER="admin"
MESHCTRL_PASS=""
MESHCTRL_DOMAIN=""
MESHCTRL_LOGIN_KEY_FILE="/etc/gracepress/meshcentral-login.key"
MESHCTRL_PATH=""
MESHCTRL_IGNORE_CERT="0"
MESHPRESS_LINK_HIDE=""
MESHPRESS_LINK_MODE="gotonode"
MESHPRESS_ALLOW_COMMANDS="0"
WP_DOMAIN="https://christp2p.com"
WP_API_KEY="same-token-used-in-meshpress"
MESHPRESS_PUSH_INTERVAL_SECONDS="3600"
MESHPRESS_PUSH_EVENTS="1"
```
If auto-detection cannot find `meshctrl`, set `MESHCTRL_PATH` to the full path of `meshctrl.js`.
When `WP_DOMAIN` and `WP_API_KEY` are set, the bridge pushes fleet snapshots to WordPress at startup and then every hour.
When `MESHPRESS_PUSH_EVENTS` is enabled, selected bridge lifecycle events are posted to MeshPress so WordPress can record them through OmniLog.
## Public Exposure Hardening
Do not expose port `3099` to the public internet without firewall/VPN controls. Preferred options:
1. Bind to localhost and proxy privately:
```bash
MESHPRESS_BRIDGE_BIND="127.0.0.1"
```
2. Bind to a Tailscale address only:
```bash
MESHPRESS_BRIDGE_BIND="100.x.y.z"
```
3. If the bridge must listen on all interfaces temporarily, restrict source IPs at both the firewall and app layer:
```bash
MESHPRESS_BRIDGE_BIND="0.0.0.0"
MESHPRESS_ALLOWED_IPS="WORDPRESS_SERVER_IP,YOUR_ADMIN_IP"
```
The app-layer allowlist only accepts exact IP addresses. It is a backstop, not a replacement for firewall rules.
## Diagnostics
Use MeshPress > Diagnostics & Manual Actions first. It calls these bridge endpoints:
```bash
curl -H "Authorization: Bearer TOKEN" http://127.0.0.1:3099/api/health
curl -H "Authorization: Bearer TOKEN" http://127.0.0.1:3099/api/version
curl -H "Authorization: Bearer TOKEN" http://127.0.0.1:3099/api/status
curl -H "Authorization: Bearer TOKEN" http://127.0.0.1:3099/api/self-test
curl -H "Authorization: Bearer TOKEN" http://127.0.0.1:3099/api/groups?refresh=1
curl -H "Authorization: Bearer TOKEN" http://127.0.0.1:3099/api/devices?refresh=1
```
`/api/self-test` verifies the bridge can find `meshctrl`, has an auth method, has a public MeshCentral URL, and can list device groups and devices.
For direct session links, test both URL styles:
```bash
curl -H "Authorization: Bearer TOKEN" "http://127.0.0.1:3099/api/session-links/NODEID"
curl -H "Authorization: Bearer TOKEN" "http://127.0.0.1:3099/api/link/console/NODEID?link_mode=all"
```
To trigger a catalog-managed bridge update from MeshPress admin or curl:
```bash
curl -X POST -H "Authorization: Bearer TOKEN" http://127.0.0.1:3099/api/update-now
```
To test the first provisioning path, enable commands and create a temporary group:
```bash
curl -X POST \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"MeshPress Test Group","description":"Manual bridge validation"}' \
http://127.0.0.1:3099/api/groups/create
```
## MeshCentral Auth
The bridge supports either a MeshCentral login key file or a dedicated integration username/password.
For username/password auth, set:
```bash
MESHCTRL_USER="your-generated-integration-user"
MESHCTRL_PASS="your-generated-integration-password"
```
If `MESHCTRL_LOGIN_KEY_FILE` exists, the bridge will prefer it. If the file does not exist and `MESHCTRL_PASS` is set, it will use username/password auth.
## MeshCentral Login Key Option
On the MeshCentral server:
```bash
cd /opt/meshcentral
sudo node node_modules/meshcentral --loginTokenKey > /etc/gracepress/meshcentral-login.key
sudo chmod 600 /etc/gracepress/meshcentral-login.key
```
MeshCentral must have login tokens enabled in `meshcentral-data/config.json`:
```json
{
"settings": {
"allowLoginToken": true
}
}
```
Restart MeshCentral after changing config.
+117
View File
@@ -0,0 +1,117 @@
#!/bin/bash
set -euo pipefail
COMPONENT="meshpress-bridge"
TARGET_DIR="/opt/gracepress-meshpress-bridge"
ENV_DIR="/etc/gracepress"
ENV_FILE="$ENV_DIR/meshpress-bridge.env"
CRON_FILE="/etc/cron.d/gracepress-meshpress-bridge"
SERVICE_NAME="gp-meshpress-bridge"
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VERSION="$(grep -m 1 '^# Version:' "$SOURCE_DIR/meshpress-bridge" | sed -E 's/^# Version:[[:space:]]*//')"
SOURCE_REAL="$(realpath "$SOURCE_DIR")"
TARGET_REAL="$(realpath -m "$TARGET_DIR")"
if [ "$(id -u)" -ne 0 ]; then
echo "Please run as root: sudo ./install.sh"
exit 1
fi
if [ "$SOURCE_REAL" = "$TARGET_REAL" ]; then
echo "Same-directory install detected; delegating to safe in-place updater."
exec "$TARGET_DIR/update.sh"
fi
mkdir -p "$ENV_DIR"
touch "$ENV_FILE"
chmod 600 "$ENV_FILE"
rand_token() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 32
else
date +%s%N | sha256sum | awk '{print $1}'
fi
}
ensure_env() {
local key="$1"
local value="$2"
if ! grep -q "^${key}=" "$ENV_FILE"; then
echo "${key}=\"${value}\"" >> "$ENV_FILE"
fi
}
ensure_env "MESHPRESS_BRIDGE_PORT" "${MESHPRESS_BRIDGE_PORT:-3099}"
ensure_env "MESHPRESS_BRIDGE_BIND" "${MESHPRESS_BRIDGE_BIND:-127.0.0.1}"
ensure_env "MESHPRESS_BRIDGE_TOKEN" "${MESHPRESS_BRIDGE_TOKEN:-$(rand_token)}"
ensure_env "MESHPRESS_ALLOWED_IPS" "${MESHPRESS_ALLOWED_IPS:-}"
ensure_env "MESHPRESS_AUTH_WINDOW_SECONDS" "${MESHPRESS_AUTH_WINDOW_SECONDS:-900}"
ensure_env "MESHPRESS_AUTH_MAX_FAILURES" "${MESHPRESS_AUTH_MAX_FAILURES:-30}"
ensure_env "MESHCTRL_URL" "${MESHCTRL_URL:-wss://127.0.0.1}"
ensure_env "PUBLIC_MESH_URL" "${PUBLIC_MESH_URL:-https://mesh.example.com}"
ensure_env "MESHCTRL_USER" "${MESHCTRL_USER:-admin}"
ensure_env "MESHCTRL_PASS" "${MESHCTRL_PASS:-}"
ensure_env "MESHCTRL_DOMAIN" "${MESHCTRL_DOMAIN:-}"
ensure_env "MESHCTRL_LOGIN_KEY_FILE" "${MESHCTRL_LOGIN_KEY_FILE:-/etc/gracepress/meshcentral-login.key}"
ensure_env "MESHCTRL_PATH" "${MESHCTRL_PATH:-}"
ensure_env "MESHCTRL_IGNORE_CERT" "${MESHCTRL_IGNORE_CERT:-0}"
ensure_env "MESHPRESS_CACHE_TTL_SECONDS" "${MESHPRESS_CACHE_TTL_SECONDS:-60}"
ensure_env "MESHPRESS_LINK_HIDE" "${MESHPRESS_LINK_HIDE:-}"
ensure_env "MESHPRESS_ALLOW_COMMANDS" "${MESHPRESS_ALLOW_COMMANDS:-0}"
ensure_env "MESHPRESS_ALLOW_MAINTENANCE" "${MESHPRESS_ALLOW_MAINTENANCE:-0}"
ensure_env "MESHCENTRAL_DIR" "${MESHCENTRAL_DIR:-}"
ensure_env "MESHCENTRAL_DATA_DIR" "${MESHCENTRAL_DATA_DIR:-}"
ensure_env "MESHCENTRAL_SERVICE_NAME" "${MESHCENTRAL_SERVICE_NAME:-meshcentral}"
ensure_env "MESHPRESS_BACKUP_DIR" "${MESHPRESS_BACKUP_DIR:-/var/backups/gracepress-meshcentral}"
ensure_env "WP_DOMAIN" "${WP_DOMAIN:-}"
ensure_env "WP_API_KEY" "${WP_API_KEY:-}"
ensure_env "MESHPRESS_PUSH_INTERVAL_SECONDS" "${MESHPRESS_PUSH_INTERVAL_SECONDS:-3600}"
ensure_env "MESHPRESS_PUSH_EVENTS" "${MESHPRESS_PUSH_EVENTS:-1}"
ensure_env "AUTO_UPDATE" "${AUTO_UPDATE:-1}"
apt-get update -y >/dev/null
apt-get install -y curl ca-certificates >/dev/null
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null
apt-get install -y nodejs >/dev/null
npm install -g pm2 >/dev/null
pm2 install pm2-logrotate >/dev/null 2>&1 || true
pm2 set pm2-logrotate:max_size 25M >/dev/null 2>&1 || true
pm2 set pm2-logrotate:retain 5 >/dev/null 2>&1 || true
STAGE_DIR="${TARGET_DIR}.installing.$$"
rm -rf "$STAGE_DIR"
mkdir -p "$STAGE_DIR"
cp -a "$SOURCE_DIR"/. "$STAGE_DIR"/
echo "$VERSION" > "$STAGE_DIR/VERSION"
cd "$STAGE_DIR"
npm install --omit=dev >/dev/null
node --check server.js >/dev/null
chmod +x "$STAGE_DIR/update.sh" "$STAGE_DIR/meshpress-bridge"
pm2 delete "$SERVICE_NAME" >/dev/null 2>&1 || true
if [ -d "$TARGET_DIR" ]; then
BACKUP_DIR="${TARGET_DIR}.old.$(date +%Y%m%d%H%M%S)"
mv "$TARGET_DIR" "$BACKUP_DIR"
echo "Previous install moved to $BACKUP_DIR"
fi
mv "$STAGE_DIR" "$TARGET_DIR"
cd "$TARGET_DIR"
pm2 start server.js --name "$SERVICE_NAME" --time --max-memory-restart 512M >/dev/null
pm2 save >/dev/null
pm2 startup | grep "sudo" | bash >/dev/null 2>&1 || true
cat > "$CRON_FILE" <<EOF
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
17 * * * * root [ -f "$ENV_FILE" ] && . "$ENV_FILE"; [ "\${AUTO_UPDATE:-1}" = "1" ] && "$TARGET_DIR/update.sh" >/var/log/gracepress-meshpress-bridge-update.log 2>&1
EOF
chmod 644 "$CRON_FILE"
echo "GracePress MeshPress Bridge v$VERSION installed."
echo "Config: $ENV_FILE"
echo "Token: $(grep '^MESHPRESS_BRIDGE_TOKEN=' "$ENV_FILE" | cut -d= -f2- | sed 's/^\"//;s/\"$//')"
echo "Next: edit $ENV_FILE and set MESHCTRL_URL, PUBLIC_MESH_URL, MESHCTRL_USER, MESHCTRL_PASS or MESHCTRL_LOGIN_KEY_FILE, MESHCTRL_PATH, WP_DOMAIN, and WP_API_KEY."
echo "Then restart: pm2 restart $SERVICE_NAME --update-env"
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# Description: GracePress MeshPress Bridge. Pushes MeshCentral fleet snapshots to WordPress MeshPress.
# Category: Hidden
# Version: 0.4.3
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec node "$DIR/server.js" "$@"
+15
View File
@@ -0,0 +1,15 @@
{
"name": "gracepress-meshpress-bridge",
"version": "0.4.3",
"private": true,
"description": "GracePress bridge service for MeshCentral fleet sync and device links",
"main": "server.js",
"scripts": {
"start": "node server.js",
"check": "node --check server.js"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.3"
}
}
File diff suppressed because it is too large Load Diff
+109
View File
@@ -0,0 +1,109 @@
#!/bin/bash
set -euo pipefail
COMPONENT="meshpress-bridge"
TARGET_DIR="/opt/gracepress-meshpress-bridge"
ENV_FILE="/etc/gracepress/meshpress-bridge.env"
SERVICE_NAME="gp-meshpress-bridge"
LOG_PREFIX="[meshpress-bridge-update]"
LOCK_FILE="/tmp/gracepress-meshpress-bridge-update.lock"
TMP_DIR="$(mktemp -d)"
STAGE_DIR=""
BACKUP_DIR=""
cleanup() {
rm -rf "$TMP_DIR"
if [ -n "$STAGE_DIR" ] && [ -d "$STAGE_DIR" ]; then rm -rf "$STAGE_DIR"; fi
}
trap cleanup EXIT
log() { echo "$LOG_PREFIX $(date -Is) $*"; }
if [ "$(id -u)" -ne 0 ]; then
log "Please run as root."
exit 1
fi
exec 9>"$LOCK_FILE"
if command -v flock >/dev/null 2>&1; then
flock -n 9 || { log "Another bridge update is already running."; exit 1; }
fi
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
CATALOG_URL="${GRACEPRESS_CATALOG_URL:-https://christit.com/gracepress/api.php?endpoint=manifest&slug=meshpress-bridge}"
apt-get update -y >/dev/null
apt-get install -y curl unzip python3 ca-certificates nodejs npm >/dev/null
if ! command -v pm2 >/dev/null 2>&1; then
npm install -g pm2 >/dev/null
fi
log "Checking catalog: $CATALOG_URL"
curl -fsSL "$CATALOG_URL&nocache=$(date +%s)" -o "$TMP_DIR/manifest.json"
DOWNLOAD_URL="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("download_url",""))' "$TMP_DIR/manifest.json")"
VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("version","0.0.0"))' "$TMP_DIR/manifest.json")"
if [ -z "$DOWNLOAD_URL" ]; then
log "No download_url found in catalog manifest."
exit 1
fi
CURRENT="$(cat "$TARGET_DIR/VERSION" 2>/dev/null || echo 0.0.0)"
if [ "$CURRENT" = "$VERSION" ]; then
log "$COMPONENT is already current: $CURRENT"
exit 0
fi
log "Updating $COMPONENT from $CURRENT to $VERSION"
curl -fL "$DOWNLOAD_URL" -o "$TMP_DIR/package.zip"
unzip -q "$TMP_DIR/package.zip" -d "$TMP_DIR/pkg"
PKG_DIR="$(find "$TMP_DIR/pkg" -maxdepth 3 -type f -name server.js -printf '%h\n' | head -n 1)"
if [ -z "$PKG_DIR" ] || [ ! -f "$PKG_DIR/package.json" ]; then
log "Package does not contain a valid bridge payload."
exit 1
fi
STAGE_DIR="${TARGET_DIR}.next.${VERSION}.$$"
rm -rf "$STAGE_DIR"
mkdir -p "$STAGE_DIR"
cp -a "$PKG_DIR"/. "$STAGE_DIR"/
echo "$VERSION" > "$STAGE_DIR/VERSION"
chmod +x "$STAGE_DIR/update.sh" "$STAGE_DIR/meshpress-bridge" 2>/dev/null || true
log "Installing node dependencies in staging directory."
(cd "$STAGE_DIR" && npm install --omit=dev >/dev/null && node --check server.js >/dev/null)
BACKUP_DIR="${TARGET_DIR}.old.$(date +%Y%m%d%H%M%S)"
if [ -d "$TARGET_DIR" ]; then
mv "$TARGET_DIR" "$BACKUP_DIR"
log "Previous install moved to $BACKUP_DIR"
fi
mv "$STAGE_DIR" "$TARGET_DIR"
STAGE_DIR=""
rollback() {
log "Update failed after swap; attempting rollback."
local failed="${TARGET_DIR}.failed.$(date +%Y%m%d%H%M%S)"
if [ -d "$TARGET_DIR" ]; then mv "$TARGET_DIR" "$failed"; fi
if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then mv "$BACKUP_DIR" "$TARGET_DIR"; fi
if [ -f "$TARGET_DIR/server.js" ]; then
pm2 start "$TARGET_DIR/server.js" --name "$SERVICE_NAME" --time --max-memory-restart 512M >/dev/null 2>&1 || pm2 restart "$SERVICE_NAME" --update-env >/dev/null 2>&1 || true
pm2 save >/dev/null 2>&1 || true
fi
}
if pm2 describe "$SERVICE_NAME" >/dev/null 2>&1; then
if ! (cd "$TARGET_DIR" && pm2 restart "$SERVICE_NAME" --update-env >/dev/null); then
rollback
exit 1
fi
else
if ! (cd "$TARGET_DIR" && pm2 start server.js --name "$SERVICE_NAME" --time --max-memory-restart 512M >/dev/null); then
rollback
exit 1
fi
fi
pm2 save >/dev/null
log "$COMPONENT updated to $VERSION"