refactor: optimize startup, improve security, and simplify n8n configuration for v2 compatibility
Browse files- .env.example +9 -61
- Dockerfile +3 -0
- README.md +20 -186
- health-server.js +236 -79
- n8n-sync.py +7 -8
- start.sh +21 -23
.env.example
CHANGED
|
@@ -1,33 +1,10 @@
|
|
| 1 |
-
#
|
| 2 |
-
# Hugging8n β Environment Variables Reference
|
| 3 |
-
# Copy to .env and fill in your values before running locally.
|
| 4 |
-
# On HuggingFace Spaces, add Secrets and Variables in Settings β Variables and secrets.
|
| 5 |
-
# =============================================================================
|
| 6 |
-
|
| 7 |
-
# -----------------------------------------------------------------------------
|
| 8 |
-
# REQUIRED β Basic Auth (set these as Secrets on HF Spaces)
|
| 9 |
-
# -----------------------------------------------------------------------------
|
| 10 |
-
|
| 11 |
-
# Username for n8n basic auth login
|
| 12 |
-
N8N_BASIC_AUTH_USER=admin
|
| 13 |
-
|
| 14 |
-
# Password for n8n basic auth login. REQUIRED to protect your instance.
|
| 15 |
-
# Generate a strong password: openssl rand -base64 24
|
| 16 |
-
N8N_BASIC_AUTH_PASSWORD=
|
| 17 |
-
|
| 18 |
-
# -----------------------------------------------------------------------------
|
| 19 |
-
# RECOMMENDED β Persistent Backup (set these as Secrets on HF Spaces)
|
| 20 |
-
# -----------------------------------------------------------------------------
|
| 21 |
-
|
| 22 |
-
# Your Hugging Face write token (to create/update the backup dataset)
|
| 23 |
-
# Get one at: https://huggingface.co/settings/tokens
|
| 24 |
HF_TOKEN=
|
| 25 |
|
| 26 |
-
# Your Hugging Face username (
|
| 27 |
-
# If not set, inferred from SPACE_AUTHOR_NAME automatically on HF Spaces
|
| 28 |
HF_USERNAME=
|
| 29 |
|
| 30 |
-
# Name of the private dataset repo used for backup
|
| 31 |
BACKUP_DATASET_NAME=hugging8n-backup
|
| 32 |
|
| 33 |
# How often to sync the backup, in seconds
|
|
@@ -37,59 +14,30 @@ SYNC_INTERVAL=180
|
|
| 37 |
# OPTIONAL β n8n Configuration
|
| 38 |
# -----------------------------------------------------------------------------
|
| 39 |
|
| 40 |
-
# Timezone for n8n
|
| 41 |
GENERIC_TIMEZONE=UTC
|
| 42 |
|
| 43 |
# n8n encryption key β protects stored credentials. Persisted in backup.
|
| 44 |
# Generate with: openssl rand -hex 32
|
| 45 |
-
# WARNING: if you lose this key and your backup, all credentials are lost.
|
| 46 |
N8N_ENCRYPTION_KEY=
|
| 47 |
|
| 48 |
-
# Override the auto-detected Space hostname (leave blank on HF Spaces)
|
| 49 |
-
SPACE_HOST_OVERRIDE=
|
| 50 |
-
|
| 51 |
# -----------------------------------------------------------------------------
|
| 52 |
-
#
|
| 53 |
# -----------------------------------------------------------------------------
|
| 54 |
|
| 55 |
-
# UptimeRobot Main API key for creating an external keep-alive monitor
|
| 56 |
-
# Get one at: https://uptimerobot.com β My Settings β API Settings β Main API Key
|
| 57 |
-
# This is NOT needed as a HF Space Secret β it's only used by setup-uptimerobot.sh
|
| 58 |
-
UPTIMEROBOT_API_KEY=
|
| 59 |
-
|
| 60 |
-
# Custom monitor name (defaults to "Hugging8n <space-host>")
|
| 61 |
-
UPTIMEROBOT_MONITOR_NAME=
|
| 62 |
-
|
| 63 |
-
# UptimeRobot ping interval in minutes (default: 5)
|
| 64 |
-
UPTIMEROBOT_INTERVAL=5
|
| 65 |
-
|
| 66 |
-
# Comma-separated UptimeRobot alert contact IDs (optional)
|
| 67 |
-
UPTIMEROBOT_ALERT_CONTACTS=
|
| 68 |
-
|
| 69 |
-
# -----------------------------------------------------------------------------
|
| 70 |
-
# ADVANCED β n8n Internals (usually leave as defaults)
|
| 71 |
-
# -----------------------------------------------------------------------------
|
| 72 |
-
|
| 73 |
-
# Disable n8n basic auth (not recommended β leaves your instance unprotected)
|
| 74 |
-
# N8N_BASIC_AUTH_ACTIVE=false
|
| 75 |
-
|
| 76 |
# Internal n8n port (default: 5678)
|
| 77 |
N8N_PORT=5678
|
| 78 |
|
| 79 |
# Public proxy port exposed by HF Spaces (default: 7861)
|
| 80 |
PUBLIC_PORT=7861
|
| 81 |
|
| 82 |
-
# Disable
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
# Disable n8n personalization prompts (default: false)
|
| 86 |
-
N8N_PERSONALIZATION_ENABLED=false
|
| 87 |
|
| 88 |
# -----------------------------------------------------------------------------
|
| 89 |
# BUILD-TIME VARIABLE (HF Spaces: add as Variable, not Secret)
|
| 90 |
# -----------------------------------------------------------------------------
|
| 91 |
|
| 92 |
-
# Pin a specific n8n version for reproducibility
|
| 93 |
-
# Example: 1.90.0
|
| 94 |
-
# On HF Spaces, add this as a Variable (not Secret) so it's passed as a build arg.
|
| 95 |
N8N_VERSION=latest
|
|
|
|
| 1 |
+
# Your Hugging Face write token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
HF_TOKEN=
|
| 3 |
|
| 4 |
+
# Your Hugging Face username (inferred automatically on HF Spaces)
|
|
|
|
| 5 |
HF_USERNAME=
|
| 6 |
|
| 7 |
+
# Name of the private dataset repo used for backup
|
| 8 |
BACKUP_DATASET_NAME=hugging8n-backup
|
| 9 |
|
| 10 |
# How often to sync the backup, in seconds
|
|
|
|
| 14 |
# OPTIONAL β n8n Configuration
|
| 15 |
# -----------------------------------------------------------------------------
|
| 16 |
|
| 17 |
+
# Timezone for n8n triggers (e.g. Asia/Dhaka)
|
| 18 |
GENERIC_TIMEZONE=UTC
|
| 19 |
|
| 20 |
# n8n encryption key β protects stored credentials. Persisted in backup.
|
| 21 |
# Generate with: openssl rand -hex 32
|
|
|
|
| 22 |
N8N_ENCRYPTION_KEY=
|
| 23 |
|
|
|
|
|
|
|
|
|
|
| 24 |
# -----------------------------------------------------------------------------
|
| 25 |
+
# ADVANCED β n8n Internals
|
| 26 |
# -----------------------------------------------------------------------------
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
# Internal n8n port (default: 5678)
|
| 29 |
N8N_PORT=5678
|
| 30 |
|
| 31 |
# Public proxy port exposed by HF Spaces (default: 7861)
|
| 32 |
PUBLIC_PORT=7861
|
| 33 |
|
| 34 |
+
# Disable unnecessary services
|
| 35 |
+
N8N_RUNNERS_ENABLED=false
|
| 36 |
+
N8N_LICENSE_AUTO_RENEW_ENABLED=false
|
|
|
|
|
|
|
| 37 |
|
| 38 |
# -----------------------------------------------------------------------------
|
| 39 |
# BUILD-TIME VARIABLE (HF Spaces: add as Variable, not Secret)
|
| 40 |
# -----------------------------------------------------------------------------
|
| 41 |
|
| 42 |
+
# Pin a specific n8n version for reproducibility
|
|
|
|
|
|
|
| 43 |
N8N_VERSION=latest
|
Dockerfile
CHANGED
|
@@ -37,5 +37,8 @@ USER node
|
|
| 37 |
|
| 38 |
EXPOSE 7861
|
| 39 |
|
|
|
|
|
|
|
|
|
|
| 40 |
ENTRYPOINT ["/usr/bin/tini", "--"]
|
| 41 |
CMD ["/home/node/app/start.sh"]
|
|
|
|
| 37 |
|
| 38 |
EXPOSE 7861
|
| 39 |
|
| 40 |
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s \
|
| 41 |
+
CMD curl -f http://localhost:7861/health || exit 1
|
| 42 |
+
|
| 43 |
ENTRYPOINT ["/usr/bin/tini", "--"]
|
| 44 |
CMD ["/home/node/app/start.sh"]
|
README.md
CHANGED
|
@@ -8,43 +8,22 @@ app_port: 7861
|
|
| 8 |
pinned: true
|
| 9 |
license: mit
|
| 10 |
secrets:
|
| 11 |
-
- name: N8N_BASIC_AUTH_PASSWORD
|
| 12 |
-
description: Password to log in to your n8n instance. Required to protect your workflows.
|
| 13 |
- name: HF_TOKEN
|
| 14 |
-
description: HuggingFace token with write access. Used for automatic backup
|
| 15 |
---
|
| 16 |
|
| 17 |
-
|
| 18 |
-
[](https://github.com/somratpro/Hugging8N)
|
| 19 |
-
[](https://opensource.org/licenses/MIT)
|
| 20 |
-
[](https://huggingface.co/spaces/somratpro/Hugging8n)
|
| 21 |
-
[](https://n8n.io)
|
| 22 |
|
| 23 |
-
**Self-hosted n8n workflow automation β free, no server needed.** Hugging8n runs [n8n](https://n8n.io) on HuggingFace Spaces Docker,
|
| 24 |
-
|
| 25 |
-
## Table of Contents
|
| 26 |
-
|
| 27 |
-
- [β¨ Features](#-features)
|
| 28 |
-
- [π Quick Start](#-quick-start)
|
| 29 |
-
- [π Authentication](#-authentication)
|
| 30 |
-
- [πΎ Persistent Backup](#-persistent-backup)
|
| 31 |
-
- [π Staying Alive](#-staying-alive)
|
| 32 |
-
- [ποΈ Architecture](#-architecture)
|
| 33 |
-
- [π» Local Development](#-local-development)
|
| 34 |
-
- [π Troubleshooting](#-troubleshooting)
|
| 35 |
-
- [π€ Contributing](#-contributing)
|
| 36 |
-
- [π License](#-license)
|
| 37 |
|
| 38 |
## β¨ Features
|
| 39 |
|
| 40 |
-
- β‘ **Zero Config:** Duplicate this Space, set
|
| 41 |
-
-
|
| 42 |
-
-
|
| 43 |
-
-
|
| 44 |
-
-
|
| 45 |
-
- β° **
|
| 46 |
-
- π **Health Endpoint:** `/health` returns service and sync status for monitoring.
|
| 47 |
-
- π **100% HF-Native:** Runs entirely on HuggingFace's free infrastructure.
|
| 48 |
|
| 49 |
## π Quick Start
|
| 50 |
|
|
@@ -52,173 +31,28 @@ secrets:
|
|
| 52 |
|
| 53 |
[](https://huggingface.co/spaces/somratpro/Hugging8n?duplicate=true)
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
### Step 2: Add Your Secrets
|
| 58 |
-
|
| 59 |
-
Navigate to your new Space's **Settings**, scroll to the **Variables and secrets** section, and add the following under **Secrets**:
|
| 60 |
-
|
| 61 |
-
#### Required β Authentication
|
| 62 |
-
|
| 63 |
-
| Secret | Description |
|
| 64 |
-
| :--- | :--- |
|
| 65 |
-
| `N8N_BASIC_AUTH_PASSWORD` | Password to log in to your n8n instance. Set this to protect your workflows. |
|
| 66 |
-
|
| 67 |
-
#### Required β Persistent Backup *(Highly Recommended)*
|
| 68 |
-
|
| 69 |
-
| Secret | Description |
|
| 70 |
-
| :--- | :--- |
|
| 71 |
-
| `HF_TOKEN` | HuggingFace token with write access. Get one at [hf.co/settings/tokens](https://huggingface.co/settings/tokens). |
|
| 72 |
-
|
| 73 |
-
> [!TIP]
|
| 74 |
-
> Without `HF_TOKEN`, n8n will still run, but workflows and credentials will be **lost every time the Space restarts**. It is strongly recommended to set this.
|
| 75 |
-
|
| 76 |
-
#### Optional β Configuration
|
| 77 |
|
| 78 |
-
|
| 79 |
-
| :--- | :--- | :--- |
|
| 80 |
-
| `N8N_BASIC_AUTH_USER` | `admin` | Username for n8n login |
|
| 81 |
-
| `HF_USERNAME` | *(auto-detected)* | Your HF username, for naming the backup dataset |
|
| 82 |
-
| `BACKUP_DATASET_NAME` | `hugging8n-backup` | Name of the private dataset repo for backup |
|
| 83 |
-
| `SYNC_INTERVAL` | `180` | How often to back up, in seconds |
|
| 84 |
-
| `GENERIC_TIMEZONE` | `UTC` | Timezone for schedule triggers (e.g. `Asia/Dhaka`) |
|
| 85 |
-
| `N8N_ENCRYPTION_KEY` | *(auto-generated)* | Encryption key for stored credentials. Set explicitly so it survives Space rebuilds. |
|
| 86 |
|
| 87 |
-
###
|
| 88 |
|
| 89 |
-
|
| 90 |
-
| :--- | :--- | :--- |
|
| 91 |
-
| `N8N_VERSION` | `latest` | Pin a specific n8n version (e.g. `1.90.0`) for reproducibility |
|
| 92 |
-
|
| 93 |
-
> [!IMPORTANT]
|
| 94 |
-
> On HuggingFace Spaces, `N8N_VERSION` must be added as a **Variable** (not a Secret) so it is passed as a Docker build arg during image build.
|
| 95 |
-
|
| 96 |
-
### Step 3: Deploy & Run
|
| 97 |
-
|
| 98 |
-
That's it! The Space will build and start automatically. Watch progress in the **Logs** tab. First build takes a few minutes as n8n installs.
|
| 99 |
-
|
| 100 |
-
### Step 4: Log In
|
| 101 |
-
|
| 102 |
-
Once the Space is running, open it and log in with:
|
| 103 |
-
- **Username:** the value of `N8N_BASIC_AUTH_USER` (default: `admin`)
|
| 104 |
-
- **Password:** the value of `N8N_BASIC_AUTH_PASSWORD`
|
| 105 |
-
|
| 106 |
-
> [!WARNING]
|
| 107 |
-
> If you did not set `N8N_BASIC_AUTH_PASSWORD`, your n8n instance is **unprotected**. Anyone with the Space URL can access your workflows and credentials. Set this secret immediately.
|
| 108 |
|
| 109 |
## π Authentication
|
| 110 |
|
| 111 |
-
Hugging8n uses n8n's
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
| :--- | :--- | :--- |
|
| 115 |
-
| `N8N_BASIC_AUTH_ACTIVE` | `true` | Set to `false` to disable basic auth (not recommended) |
|
| 116 |
-
| `N8N_BASIC_AUTH_USER` | `admin` | Login username |
|
| 117 |
-
| `N8N_BASIC_AUTH_PASSWORD` | *(none)* | Login password β **must be set** |
|
| 118 |
|
| 119 |
## πΎ Persistent Backup
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
**What is backed up:**
|
| 124 |
-
- All workflows
|
| 125 |
-
- All credentials (encrypted)
|
| 126 |
-
- SQLite database (hot-copy via `sqlite3 .backup`)
|
| 127 |
-
- n8n encryption key
|
| 128 |
-
- User data
|
| 129 |
-
|
| 130 |
-
**How it works:**
|
| 131 |
-
1. On startup: restore from dataset (if it exists)
|
| 132 |
-
2. Every `SYNC_INTERVAL` seconds: detect changes and upload
|
| 133 |
-
3. On shutdown (`SIGTERM`): run a final backup before exiting
|
| 134 |
-
|
| 135 |
-
| Variable | Default | Description |
|
| 136 |
-
| :--- | :--- | :--- |
|
| 137 |
-
| `HF_TOKEN` | β | HF write token |
|
| 138 |
-
| `HF_USERNAME` | *(auto)* | Dataset owner username |
|
| 139 |
-
| `BACKUP_DATASET_NAME` | `hugging8n-backup` | Dataset repo name |
|
| 140 |
-
| `SYNC_INTERVAL` | `180` | Backup interval in seconds |
|
| 141 |
-
|
| 142 |
-
> [!TIP]
|
| 143 |
-
> Set `N8N_ENCRYPTION_KEY` explicitly. If n8n auto-generates it and the Space is rebuilt (not just restarted), the key will be different and your backed-up credentials will be unreadable.
|
| 144 |
-
|
| 145 |
-
## π Staying Alive *(Recommended on Free HF Spaces)*
|
| 146 |
-
|
| 147 |
-
Free HF Spaces sleep after periods of inactivity. Set up an external UptimeRobot monitor to keep yours awake.
|
| 148 |
-
|
| 149 |
-
1. Create a free account at [uptimerobot.com](https://uptimerobot.com)
|
| 150 |
-
2. Get your **Main API Key** from My Settings β API Settings
|
| 151 |
-
3. Run the helper script:
|
| 152 |
-
```bash
|
| 153 |
-
UPTIMEROBOT_API_KEY=your-key ./setup-uptimerobot.sh your-space.hf.space
|
| 154 |
-
```
|
| 155 |
-
4. UptimeRobot will ping `/health` every 5 minutes from outside HF, keeping the Space awake.
|
| 156 |
-
|
| 157 |
-
> [!NOTE]
|
| 158 |
-
> This works for **public** Spaces only. Private Spaces cannot be pinged by external monitors.
|
| 159 |
|
| 160 |
## ποΈ Architecture
|
| 161 |
|
| 162 |
-
``
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
βββ start.sh # Startup orchestrator: restore β sync loop β proxy β n8n
|
| 166 |
-
βββ n8n-sync.py # Backup/restore via huggingface_hub
|
| 167 |
-
βββ health-server.js # HTTP + WebSocket reverse proxy (port 7861 β 5678)
|
| 168 |
-
βββ setup-uptimerobot.sh # One-shot UptimeRobot monitor creation
|
| 169 |
-
βββ .env.example # All environment variable documentation
|
| 170 |
-
βββ README.md # This file
|
| 171 |
-
```
|
| 172 |
-
|
| 173 |
-
**Startup sequence:**
|
| 174 |
-
1. Read environment variables and set n8n config
|
| 175 |
-
2. Warn if `N8N_BASIC_AUTH_PASSWORD` is not set
|
| 176 |
-
3. Restore backup from HF Dataset (if `HF_TOKEN` is set)
|
| 177 |
-
4. Start backup sync loop in background
|
| 178 |
-
5. Start health/proxy server on port 7861
|
| 179 |
-
6. Start n8n on port 5678
|
| 180 |
-
7. On `SIGTERM` / `SIGINT`: final backup + clean exit
|
| 181 |
-
|
| 182 |
-
## π» Local Development
|
| 183 |
-
|
| 184 |
-
```bash
|
| 185 |
-
git clone https://github.com/somratpro/Hugging8N.git
|
| 186 |
-
cd Hugging8N
|
| 187 |
-
cp .env.example .env
|
| 188 |
-
# Fill in N8N_BASIC_AUTH_PASSWORD and optionally HF_TOKEN
|
| 189 |
-
```
|
| 190 |
-
|
| 191 |
-
**With Docker:**
|
| 192 |
-
|
| 193 |
-
```bash
|
| 194 |
-
docker build -t hugging8n .
|
| 195 |
-
docker run -p 7861:7861 --env-file .env hugging8n
|
| 196 |
-
```
|
| 197 |
-
|
| 198 |
-
Then open `http://localhost:7861`.
|
| 199 |
-
|
| 200 |
-
**Pin an n8n version:**
|
| 201 |
-
|
| 202 |
-
```bash
|
| 203 |
-
docker build --build-arg N8N_VERSION=1.90.0 -t hugging8n .
|
| 204 |
-
```
|
| 205 |
-
|
| 206 |
-
## π Troubleshooting
|
| 207 |
-
|
| 208 |
-
- **Can't log in:** Make sure `N8N_BASIC_AUTH_PASSWORD` is set in Secrets. Default username is `admin`.
|
| 209 |
-
- **Workflows lost after restart:** Set `HF_TOKEN` and `HF_USERNAME` so the backup dataset is created and restored.
|
| 210 |
-
- **n8n editor shows "disconnected":** The WebSocket proxy may not have connected yet β wait a few seconds and refresh. Check Space logs for errors.
|
| 211 |
-
- **Space keeps sleeping:** Use `setup-uptimerobot.sh` to set up an external keep-alive monitor.
|
| 212 |
-
- **Credentials unreadable after rebuild:** Make sure `N8N_ENCRYPTION_KEY` is set explicitly as a Secret. If it was auto-generated, it changes on rebuild.
|
| 213 |
-
- **Build fails:** If you pinned `N8N_VERSION`, verify the version exists at [npmjs.com/package/n8n](https://www.npmjs.com/package/n8n?activeTab=versions).
|
| 214 |
-
- **Backup failing:** Check Space logs for `Sync failed`. Verify `HF_TOKEN` has write access and `HF_USERNAME` is correct.
|
| 215 |
-
|
| 216 |
-
## π€ Contributing
|
| 217 |
-
|
| 218 |
-
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
| 219 |
-
|
| 220 |
-
## π License
|
| 221 |
-
|
| 222 |
-
MIT β see [LICENSE](LICENSE) for details.
|
| 223 |
|
| 224 |
*Made with β€οΈ by [@somratpro](https://github.com/somratpro)*
|
|
|
|
| 8 |
pinned: true
|
| 9 |
license: mit
|
| 10 |
secrets:
|
|
|
|
|
|
|
| 11 |
- name: HF_TOKEN
|
| 12 |
+
description: HuggingFace token with write access. Used for automatic backup.
|
| 13 |
---
|
| 14 |
|
| 15 |
+
# βΎοΈ Hugging8n
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
**Self-hosted n8n workflow automation β free, no server needed.** Hugging8n runs [n8n](https://n8n.io) on HuggingFace Spaces Docker, serving a premium dashboard at `/` and the n8n editor at `/app/`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
## β¨ Features
|
| 20 |
|
| 21 |
+
- β‘ **Zero Config:** Duplicate this Space, set `HF_TOKEN`, and you're ready.
|
| 22 |
+
- πΎ **Persistent Backup:** Workflows and credentials back up automatically to a private HF Dataset.
|
| 23 |
+
- π **Secure by Default:** Uses n8n v2's built-in user management. No more insecure environment variables.
|
| 24 |
+
- π³ **Docker Native:** Optimized for the free HF Spaces tier.
|
| 25 |
+
- π **Dashboard UI:** Beautiful management interface at the root URL.
|
| 26 |
+
- β° **Built-in Keep-Alive:** Easily setup UptimeRobot from the dashboard.
|
|
|
|
|
|
|
| 27 |
|
| 28 |
## π Quick Start
|
| 29 |
|
|
|
|
| 31 |
|
| 32 |
[](https://huggingface.co/spaces/somratpro/Hugging8n?duplicate=true)
|
| 33 |
|
| 34 |
+
### Step 2: Add Your HF_TOKEN
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
Add your HuggingFace token with **write** access to the Space Secrets. This enables automatic backup so your workflows aren't lost on restart.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
### Step 3: Set Up Auth
|
| 39 |
|
| 40 |
+
When the Space starts, visit the URL and click **Open n8n Editor**. On the first run, n8n will ask you to create an owner account. **This is your primary login.**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
## π Authentication
|
| 43 |
|
| 44 |
+
Hugging8n uses n8n's native user management.
|
| 45 |
+
1. The first person to visit `/app/` on a fresh install becomes the owner.
|
| 46 |
+
2. If you are restoring from a backup, your existing user accounts will be active.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
## πΎ Persistent Backup
|
| 49 |
|
| 50 |
+
Your data is synced to a private dataset named `hugging8n-backup` in your HF account.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
## ποΈ Architecture
|
| 53 |
|
| 54 |
+
- `/` : Premium Dashboard (Status, Uptime, Keep-Alive setup)
|
| 55 |
+
- `/app/` : n8n Workflow Editor
|
| 56 |
+
- `/health` : Health check endpoint
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
*Made with β€οΈ by [@somratpro](https://github.com/somratpro)*
|
health-server.js
CHANGED
|
@@ -1,109 +1,266 @@
|
|
| 1 |
const http = require("http");
|
| 2 |
-
const
|
| 3 |
const fs = require("fs");
|
|
|
|
| 4 |
|
| 5 |
-
const
|
| 6 |
const TARGET_PORT = Number(process.env.N8N_PORT || 5678);
|
| 7 |
const TARGET_HOST = "127.0.0.1";
|
| 8 |
-
const
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
function
|
| 11 |
try {
|
| 12 |
-
return
|
| 13 |
} catch {
|
| 14 |
-
return
|
| 15 |
-
status: "unknown",
|
| 16 |
-
message: "No sync status yet",
|
| 17 |
-
timestamp: new Date().toISOString(),
|
| 18 |
-
};
|
| 19 |
}
|
| 20 |
}
|
| 21 |
|
| 22 |
-
function
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
-
function
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
"
|
| 32 |
-
"
|
| 33 |
-
"x-forwarded-proto": req.headers["x-forwarded-proto"] || "https",
|
| 34 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
}
|
| 46 |
|
| 47 |
-
if (
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
}
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
});
|
| 72 |
|
| 73 |
-
req.pipe(
|
| 74 |
});
|
| 75 |
|
| 76 |
server.on("upgrade", (req, socket, head) => {
|
| 77 |
-
const
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
const headerLines = Object.entries(headers)
|
| 89 |
-
.map(([key, value]) => `${key}: ${value}`)
|
| 90 |
-
.join("\r\n");
|
| 91 |
-
|
| 92 |
-
upstream.write(
|
| 93 |
-
`${req.method} ${req.url} HTTP/${req.httpVersion}\r\n` +
|
| 94 |
-
headerLines +
|
| 95 |
-
"\r\n\r\n",
|
| 96 |
-
);
|
| 97 |
-
|
| 98 |
-
if (head && head.length) upstream.write(head);
|
| 99 |
-
upstream.pipe(socket);
|
| 100 |
-
socket.pipe(upstream);
|
| 101 |
});
|
| 102 |
-
|
| 103 |
-
upstream.on("error", () => socket.destroy());
|
| 104 |
-
socket.on("error", () => upstream.destroy());
|
| 105 |
});
|
| 106 |
|
| 107 |
-
server.listen(
|
| 108 |
-
console.log(`Hugging8n proxy listening on ${PUBLIC_PORT}, forwarding to ${TARGET_PORT}`);
|
| 109 |
-
});
|
|
|
|
| 1 |
const http = require("http");
|
| 2 |
+
const https = require("https");
|
| 3 |
const fs = require("fs");
|
| 4 |
+
const net = require("net");
|
| 5 |
|
| 6 |
+
const PORT = Number(process.env.PUBLIC_PORT || 7861);
|
| 7 |
const TARGET_PORT = Number(process.env.N8N_PORT || 5678);
|
| 8 |
const TARGET_HOST = "127.0.0.1";
|
| 9 |
+
const SYNC_STATUS_FILE = "/tmp/hugging8n-sync-status.json";
|
| 10 |
+
const APP_BASE = "/app";
|
| 11 |
+
const startTime = Date.now();
|
| 12 |
|
| 13 |
+
function parseRequestUrl(url) {
|
| 14 |
try {
|
| 15 |
+
return new URL(url, "http://localhost");
|
| 16 |
} catch {
|
| 17 |
+
return new URL("http://localhost/");
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
}
|
| 19 |
}
|
| 20 |
|
| 21 |
+
function getStatus() {
|
| 22 |
+
try {
|
| 23 |
+
if (fs.existsSync(SYNC_STATUS_FILE)) {
|
| 24 |
+
return JSON.parse(fs.readFileSync(SYNC_STATUS_FILE, "utf8"));
|
| 25 |
+
}
|
| 26 |
+
} catch {}
|
| 27 |
+
return { status: "unknown", message: "Initial startup...", timestamp: new Date().toISOString() };
|
| 28 |
}
|
| 29 |
|
| 30 |
+
function renderDashboard(data) {
|
| 31 |
+
const syncBadge = (status) => {
|
| 32 |
+
let cls = "status-offline";
|
| 33 |
+
if (status === "success" || status === "configured" || status === "restored") cls = "status-online";
|
| 34 |
+
if (status === "syncing" || status === "restoring") cls = "status-syncing";
|
| 35 |
+
return `<div class="status-badge ${cls}">${cls === 'status-online' ? '<div class="pulse"></div>' : ''}${String(status).toUpperCase()}</div>`;
|
|
|
|
| 36 |
};
|
| 37 |
+
|
| 38 |
+
const keepAwakeHtml = data.isPrivate
|
| 39 |
+
? `<div class="helper-summary"><strong>Private Space detected.</strong> External monitors cannot access private health URLs.</div>`
|
| 40 |
+
: `
|
| 41 |
+
<div id="uptimerobot-flow">
|
| 42 |
+
<div class="helper-summary" id="uptimerobot-summary">Setup a free monitor to prevent this Space from sleeping.</div>
|
| 43 |
+
<button id="uptimerobot-toggle" class="helper-toggle">Set Up Monitor</button>
|
| 44 |
+
<div id="uptimerobot-shell" class="hidden" style="margin-top:12px">
|
| 45 |
+
<input id="uptimerobot-key" class="helper-input" type="password" placeholder="UptimeRobot Main API Key">
|
| 46 |
+
<button id="uptimerobot-btn" class="helper-button">Create Monitor</button>
|
| 47 |
+
</div>
|
| 48 |
+
</div>
|
| 49 |
+
`;
|
| 50 |
+
|
| 51 |
+
return `
|
| 52 |
+
<!DOCTYPE html>
|
| 53 |
+
<html lang="en">
|
| 54 |
+
<head>
|
| 55 |
+
<meta charset="UTF-8">
|
| 56 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 57 |
+
<title>Hugging8n Dashboard</title>
|
| 58 |
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
|
| 59 |
+
<style>
|
| 60 |
+
:root { --bg: #0f172a; --card: rgba(30, 41, 59, 0.7); --accent: linear-gradient(135deg, #3b82f6, #8b5cf6); --text: #f8fafc; --text-dim: #94a3b8; --success: #10b981; --error: #ef4444; }
|
| 61 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 62 |
+
body { font-family: 'Outfit', sans-serif; background: var(--bg); color: var(--text); display: flex; justify-content: center; min-height: 100vh; padding: 40px 20px; background-image: radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.15) 0px, transparent 50%), radial-gradient(at 100% 0%, rgba(139, 92, 246, 0.15) 0px, transparent 50%); }
|
| 63 |
+
.dashboard { width: 100%; max-width: 600px; background: var(--card); backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 24px; padding: 40px; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5); }
|
| 64 |
+
h1 { font-size: 2.2rem; margin-bottom: 8px; background: var(--accent); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-align: center; }
|
| 65 |
+
.subtitle { color: var(--text-dim); text-align: center; font-size: 0.9rem; margin-bottom: 40px; text-transform: uppercase; letter-spacing: 1px; }
|
| 66 |
+
.stats { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 24px; }
|
| 67 |
+
.card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.05); padding: 20px; border-radius: 16px; }
|
| 68 |
+
.label { color: var(--text-dim); font-size: 0.75rem; text-transform: uppercase; margin-bottom: 8px; display: block; }
|
| 69 |
+
.value { font-size: 1.1rem; font-weight: 600; }
|
| 70 |
+
.btn { display: block; background: var(--accent); color: #fff; padding: 16px; border-radius: 16px; text-align: center; text-decoration: none; font-weight: 600; margin-top: 8px; transition: transform 0.2s; box-shadow: 0 10px 20px -5px rgba(59, 130, 246, 0.4); }
|
| 71 |
+
.btn:hover { transform: scale(1.02); }
|
| 72 |
+
.status-badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 600; }
|
| 73 |
+
.status-online { background: rgba(16, 185, 129, 0.1); color: var(--success); }
|
| 74 |
+
.status-syncing { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
|
| 75 |
+
.status-offline { background: rgba(239, 68, 68, 0.1); color: var(--error); }
|
| 76 |
+
.pulse { width: 8px; height: 8px; border-radius: 50%; background: currentColor; animation: pulse 2s infinite; }
|
| 77 |
+
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); } 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); } }
|
| 78 |
+
.helper-input { width: 100%; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); color: #fff; padding: 12px; border-radius: 12px; margin-bottom: 12px; }
|
| 79 |
+
.helper-button { background: var(--accent); color: #fff; border: 0; padding: 12px; border-radius: 12px; cursor: pointer; width: 100%; font-weight: 600; }
|
| 80 |
+
.helper-toggle { background: rgba(255,255,255,0.05); color: #fff; border: 1px solid rgba(255,255,255,0.1); padding: 8px 16px; border-radius: 12px; cursor: pointer; font-size: 0.85rem; }
|
| 81 |
+
.hidden { display: none; }
|
| 82 |
+
.helper-summary { background: rgba(255,255,255,0.03); padding: 12px; border-radius: 12px; font-size: 0.85rem; color: var(--text-dim); margin-bottom: 12px; }
|
| 83 |
+
.helper-result { margin-top: 12px; font-size: 0.85rem; padding: 10px; border-radius: 8px; display: none; }
|
| 84 |
+
.helper-result.ok { display: block; background: rgba(16, 185, 129, 0.1); color: var(--success); }
|
| 85 |
+
.helper-result.error { display: block; background: rgba(239, 68, 68, 0.1); color: var(--error); }
|
| 86 |
+
</style>
|
| 87 |
+
</head>
|
| 88 |
+
<body>
|
| 89 |
+
<div class="dashboard">
|
| 90 |
+
<h1>βΎοΈ Hugging8n</h1>
|
| 91 |
+
<p class="subtitle">Workflow Automation Space</p>
|
| 92 |
+
|
| 93 |
+
<div class="stats">
|
| 94 |
+
<div class="card"><span class="label">Uptime</span><span class="value" id="uptime">${data.uptimeHuman}</span></div>
|
| 95 |
+
<div class="card"><span class="label">n8n Port</span><span class="value">${TARGET_PORT}</span></div>
|
| 96 |
+
</div>
|
| 97 |
+
|
| 98 |
+
<div class="card" style="margin-bottom:16px">
|
| 99 |
+
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px">
|
| 100 |
+
<span class="label" style="margin-bottom:0">Sync Status</span>
|
| 101 |
+
<div id="sync-badge">${syncBadge(data.sync.status)}</div>
|
| 102 |
+
</div>
|
| 103 |
+
<div style="font-size:0.85rem; color:var(--text-dim)">
|
| 104 |
+
Last Activity: <span id="sync-time" style="color:var(--text)">${data.sync.timestamp}</span>
|
| 105 |
+
<div id="sync-msg" style="margin-top:4px">${data.sync.message}</div>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
<a href="/app/" class="btn">Open n8n Editor</a>
|
| 110 |
+
|
| 111 |
+
<div class="card" style="margin-top:24px">
|
| 112 |
+
<span class="label">Keep Alive</span>
|
| 113 |
+
${keepAwakeHtml}
|
| 114 |
+
<div id="uptimerobot-result" class="helper-result"></div>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
|
| 118 |
+
<script>
|
| 119 |
+
async function refresh() {
|
| 120 |
+
try {
|
| 121 |
+
const res = await fetch('/status' + window.location.search);
|
| 122 |
+
const d = await res.json();
|
| 123 |
+
document.getElementById('uptime').textContent = d.uptime;
|
| 124 |
+
document.getElementById('sync-time').textContent = d.sync.timestamp;
|
| 125 |
+
document.getElementById('sync-msg').textContent = d.sync.message;
|
| 126 |
+
|
| 127 |
+
const s = d.sync.status;
|
| 128 |
+
let cls = "status-offline";
|
| 129 |
+
if (s === "success" || s === "configured" || s === "restored") cls = "status-online";
|
| 130 |
+
if (s === "syncing" || s === "restoring") cls = "status-syncing";
|
| 131 |
+
document.getElementById('sync-badge').innerHTML = '<div class="status-badge ' + cls + '">' + (cls === 'status-online' ? '<div class="pulse"></div>' : '') + s.toUpperCase() + '</div>';
|
| 132 |
+
} catch (e) {}
|
| 133 |
+
}
|
| 134 |
+
setInterval(refresh, 5000);
|
| 135 |
+
|
| 136 |
+
const toggle = document.getElementById('uptimerobot-toggle');
|
| 137 |
+
if (toggle) {
|
| 138 |
+
toggle.onclick = () => document.getElementById('uptimerobot-shell').classList.toggle('hidden');
|
| 139 |
+
document.getElementById('uptimerobot-btn').onclick = async () => {
|
| 140 |
+
const key = document.getElementById('uptimerobot-key').value;
|
| 141 |
+
const res = document.getElementById('uptimerobot-result');
|
| 142 |
+
if (!key) return;
|
| 143 |
+
res.className = 'helper-result'; res.textContent = 'Creating...'; res.style.display = 'block';
|
| 144 |
+
try {
|
| 145 |
+
const r = await fetch('/uptimerobot/setup' + window.location.search, {
|
| 146 |
+
method: 'POST',
|
| 147 |
+
headers: { 'Content-Type': 'application/json' },
|
| 148 |
+
body: JSON.stringify({ apiKey: key })
|
| 149 |
+
});
|
| 150 |
+
const data = await r.json();
|
| 151 |
+
res.className = 'helper-result ' + (r.ok ? 'ok' : 'error');
|
| 152 |
+
res.textContent = data.message;
|
| 153 |
+
} catch (e) { res.className = 'helper-result error'; res.textContent = 'Connection failed'; }
|
| 154 |
+
};
|
| 155 |
+
}
|
| 156 |
+
</script>
|
| 157 |
+
</body>
|
| 158 |
+
</html>`;
|
| 159 |
}
|
| 160 |
|
| 161 |
+
async function resolveSpaceIsPrivate(req) {
|
| 162 |
+
const params = new URLSearchParams(parseRequestUrl(req.url).search);
|
| 163 |
+
const token = params.get("__sign");
|
| 164 |
+
if (!token) return false;
|
| 165 |
+
try {
|
| 166 |
+
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
|
| 167 |
+
const sub = payload.sub || "";
|
| 168 |
+
const match = sub.match(/^\/spaces\/([^/]+)\/([^/]+)$/);
|
| 169 |
+
if (!match) return false;
|
| 170 |
+
return new Promise((resolve) => {
|
| 171 |
+
https.get(`https://huggingface.co/api/spaces/${match[1]}/${match[2]}`, { headers: { "User-Agent": "Hugging8n" } }, (res) => {
|
| 172 |
+
resolve(res.statusCode === 401 || res.statusCode === 403 || res.statusCode === 404);
|
| 173 |
+
}).on("error", () => resolve(false));
|
| 174 |
});
|
| 175 |
+
} catch { return false; }
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
const server = http.createServer(async (req, res) => {
|
| 179 |
+
const url = parseRequestUrl(req.url);
|
| 180 |
+
const pathname = url.pathname;
|
| 181 |
+
|
| 182 |
+
if (pathname === "/health") {
|
| 183 |
+
res.writeHead(200, { "Content-Type": "application/json" });
|
| 184 |
+
return res.end(JSON.stringify({ status: "ok", ...getStatus() }));
|
| 185 |
}
|
| 186 |
|
| 187 |
+
if (pathname === "/status") {
|
| 188 |
+
const uptime = Math.floor((Date.now() - startTime) / 1000);
|
| 189 |
+
return res.end(JSON.stringify({
|
| 190 |
+
uptime: `${Math.floor(uptime/3600)}h ${Math.floor((uptime%3600)/60)}m`,
|
| 191 |
+
sync: getStatus()
|
| 192 |
+
}));
|
| 193 |
}
|
| 194 |
|
| 195 |
+
if (pathname === "/uptimerobot/setup" && req.method === "POST") {
|
| 196 |
+
let body = "";
|
| 197 |
+
req.on("data", c => body += c);
|
| 198 |
+
req.on("end", async () => {
|
| 199 |
+
try {
|
| 200 |
+
const { apiKey } = JSON.parse(body);
|
| 201 |
+
const host = req.headers.host;
|
| 202 |
+
const monitorUrl = `https://${host}/health`;
|
| 203 |
+
const post = (p, d) => new Promise((res, rej) => {
|
| 204 |
+
const r = https.request({ hostname: "api.uptimerobot.com", port: 443, path: p, method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" } }, r => {
|
| 205 |
+
let b = ""; r.on("data", c => b += c); r.on("end", () => res(JSON.parse(b)));
|
| 206 |
+
}); r.on("error", rej); r.write(new URLSearchParams(d).toString()); r.end();
|
| 207 |
+
});
|
| 208 |
+
|
| 209 |
+
const existing = await post("/v2/getMonitors", { api_key: apiKey, format: "json" });
|
| 210 |
+
if (existing.monitors?.some(m => m.url === monitorUrl)) {
|
| 211 |
+
res.writeHead(200); return res.end(JSON.stringify({ message: "Monitor already exists." }));
|
| 212 |
+
}
|
| 213 |
+
const created = await post("/v2/newMonitor", { api_key: apiKey, format: "json", type: "1", friendly_name: `Hugging8n ${host}`, url: monitorUrl, interval: "300" });
|
| 214 |
+
if (created.stat === "ok") {
|
| 215 |
+
res.writeHead(200); return res.end(JSON.stringify({ message: "Monitor created successfully!" }));
|
| 216 |
+
}
|
| 217 |
+
res.writeHead(400); res.end(JSON.stringify({ message: created.error?.message || "Failed to create monitor." }));
|
| 218 |
+
} catch (e) { res.writeHead(400); res.end(JSON.stringify({ message: "Invalid request." })); }
|
| 219 |
});
|
| 220 |
+
return;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
if (pathname === "/" || pathname === "/dashboard") {
|
| 224 |
+
const uptime = Math.floor((Date.now() - startTime) / 1000);
|
| 225 |
+
const isPrivate = await resolveSpaceIsPrivate(req);
|
| 226 |
+
res.writeHead(200, { "Content-Type": "text/html" });
|
| 227 |
+
return res.end(renderDashboard({
|
| 228 |
+
uptimeHuman: `${Math.floor(uptime/3600)}h ${Math.floor((uptime%3600)/60)}m`,
|
| 229 |
+
sync: getStatus(),
|
| 230 |
+
isPrivate
|
| 231 |
+
}));
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// Proxy to n8n
|
| 235 |
+
const proxyPath = pathname.startsWith(APP_BASE) ? pathname.slice(APP_BASE.length) || "/" : pathname;
|
| 236 |
+
const proxyHeaders = { ...req.headers, host: `127.0.0.1:${TARGET_PORT}`, "x-forwarded-for": req.socket.remoteAddress, "x-forwarded-proto": "https" };
|
| 237 |
+
|
| 238 |
+
const proxyReq = http.request({ hostname: TARGET_HOST, port: TARGET_PORT, path: proxyPath + url.search, method: req.method, headers: proxyHeaders }, (proxyRes) => {
|
| 239 |
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
| 240 |
+
proxyRes.pipe(res);
|
| 241 |
+
});
|
| 242 |
+
|
| 243 |
+
proxyReq.on("error", () => {
|
| 244 |
+
res.writeHead(503, { "Content-Type": "application/json" });
|
| 245 |
+
res.end(JSON.stringify({ status: "starting", message: "n8n is initializing, please wait..." }));
|
| 246 |
});
|
| 247 |
|
| 248 |
+
req.pipe(proxyReq);
|
| 249 |
});
|
| 250 |
|
| 251 |
server.on("upgrade", (req, socket, head) => {
|
| 252 |
+
const url = parseRequestUrl(req.url);
|
| 253 |
+
const proxyPath = url.pathname.startsWith(APP_BASE) ? url.pathname.slice(APP_BASE.length) || "/" : url.pathname;
|
| 254 |
+
const proxySocket = net.connect(TARGET_PORT, TARGET_HOST, () => {
|
| 255 |
+
proxySocket.write(`${req.method} ${proxyPath}${url.search} HTTP/${req.httpVersion}\r\n`);
|
| 256 |
+
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
| 257 |
+
proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`);
|
| 258 |
+
}
|
| 259 |
+
proxySocket.write("\r\n");
|
| 260 |
+
if (head && head.length) proxySocket.write(head);
|
| 261 |
+
proxySocket.pipe(socket).pipe(proxySocket);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
});
|
| 263 |
+
proxySocket.on("error", () => socket.destroy());
|
|
|
|
|
|
|
| 264 |
});
|
| 265 |
|
| 266 |
+
server.listen(PORT, "0.0.0.0", () => console.log(`Dashboard/Proxy on ${PORT} -> n8n on ${TARGET_PORT}`));
|
|
|
|
|
|
n8n-sync.py
CHANGED
|
@@ -9,6 +9,7 @@ import subprocess
|
|
| 9 |
import sys
|
| 10 |
import tempfile
|
| 11 |
import time
|
|
|
|
| 12 |
from pathlib import Path
|
| 13 |
|
| 14 |
os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
|
|
@@ -26,7 +27,7 @@ HF_USERNAME = (
|
|
| 26 |
)
|
| 27 |
BACKUP_DATASET_NAME = os.environ.get("BACKUP_DATASET_NAME", "hugging8n-backup").strip()
|
| 28 |
HF_API = HfApi(token=HF_TOKEN) if HF_TOKEN else None
|
| 29 |
-
|
| 30 |
|
| 31 |
|
| 32 |
def write_status(status: str, message: str) -> None:
|
|
@@ -187,8 +188,7 @@ def sync_once(last_fingerprint: str | None = None) -> str:
|
|
| 187 |
|
| 188 |
|
| 189 |
def handle_signal(_sig, _frame) -> None:
|
| 190 |
-
|
| 191 |
-
RUNNING = False
|
| 192 |
|
| 193 |
|
| 194 |
def loop() -> int:
|
|
@@ -198,16 +198,15 @@ def loop() -> int:
|
|
| 198 |
last_fingerprint = fingerprint_dir(N8N_HOME)
|
| 199 |
write_status("configured", f"Backup loop active with {INTERVAL}s interval.")
|
| 200 |
|
| 201 |
-
while
|
| 202 |
try:
|
| 203 |
last_fingerprint = sync_once(last_fingerprint)
|
| 204 |
except Exception as exc:
|
| 205 |
write_status("error", f"Sync failed: {exc}")
|
| 206 |
print(f"Sync failed: {exc}", file=sys.stderr)
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
time.sleep(1)
|
| 211 |
|
| 212 |
try:
|
| 213 |
sync_once(None)
|
|
|
|
| 9 |
import sys
|
| 10 |
import tempfile
|
| 11 |
import time
|
| 12 |
+
import threading
|
| 13 |
from pathlib import Path
|
| 14 |
|
| 15 |
os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
|
|
|
|
| 27 |
)
|
| 28 |
BACKUP_DATASET_NAME = os.environ.get("BACKUP_DATASET_NAME", "hugging8n-backup").strip()
|
| 29 |
HF_API = HfApi(token=HF_TOKEN) if HF_TOKEN else None
|
| 30 |
+
STOP_EVENT = threading.Event()
|
| 31 |
|
| 32 |
|
| 33 |
def write_status(status: str, message: str) -> None:
|
|
|
|
| 188 |
|
| 189 |
|
| 190 |
def handle_signal(_sig, _frame) -> None:
|
| 191 |
+
STOP_EVENT.set()
|
|
|
|
| 192 |
|
| 193 |
|
| 194 |
def loop() -> int:
|
|
|
|
| 198 |
last_fingerprint = fingerprint_dir(N8N_HOME)
|
| 199 |
write_status("configured", f"Backup loop active with {INTERVAL}s interval.")
|
| 200 |
|
| 201 |
+
while not STOP_EVENT.is_set():
|
| 202 |
try:
|
| 203 |
last_fingerprint = sync_once(last_fingerprint)
|
| 204 |
except Exception as exc:
|
| 205 |
write_status("error", f"Sync failed: {exc}")
|
| 206 |
print(f"Sync failed: {exc}", file=sys.stderr)
|
| 207 |
+
|
| 208 |
+
if STOP_EVENT.wait(INTERVAL):
|
| 209 |
+
break
|
|
|
|
| 210 |
|
| 211 |
try:
|
| 212 |
sync_once(None)
|
start.sh
CHANGED
|
@@ -1,6 +1,9 @@
|
|
| 1 |
#!/bin/bash
|
| 2 |
set -euo pipefail
|
| 3 |
|
|
|
|
|
|
|
|
|
|
| 4 |
APP_DIR="/home/node/app"
|
| 5 |
N8N_HOME="/home/node/.n8n"
|
| 6 |
N8N_PORT="${N8N_PORT:-5678}"
|
|
@@ -12,16 +15,15 @@ mkdir -p "$N8N_HOME"
|
|
| 12 |
SPACE_HOST_DETECTED="${SPACE_HOST_OVERRIDE:-${SPACE_HOST:-}}"
|
| 13 |
if [ -n "$SPACE_HOST_DETECTED" ]; then
|
| 14 |
export N8N_HOST="${N8N_HOST:-$SPACE_HOST_DETECTED}"
|
| 15 |
-
|
| 16 |
-
export
|
|
|
|
| 17 |
fi
|
| 18 |
|
| 19 |
export N8N_PORT
|
| 20 |
export N8N_PROTOCOL="${N8N_PROTOCOL:-https}"
|
| 21 |
export N8N_PROXY_HOPS="${N8N_PROXY_HOPS:-1}"
|
| 22 |
export N8N_LISTEN_ADDRESS="${N8N_LISTEN_ADDRESS:-0.0.0.0}"
|
| 23 |
-
# Must be false: HF Spaces terminates TLS at its edge; n8n sees plain HTTP internally.
|
| 24 |
-
# Secure cookies require HTTPS end-to-end and will break login on HF Spaces.
|
| 25 |
export N8N_SECURE_COOKIE="${N8N_SECURE_COOKIE:-false}"
|
| 26 |
export N8N_DIAGNOSTICS_ENABLED="${N8N_DIAGNOSTICS_ENABLED:-false}"
|
| 27 |
export N8N_PERSONALIZATION_ENABLED="${N8N_PERSONALIZATION_ENABLED:-false}"
|
|
@@ -30,16 +32,11 @@ export N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS="${N8N_ENFORCE_SETTINGS_FILE_PERMIS
|
|
| 30 |
export GENERIC_TIMEZONE="${GENERIC_TIMEZONE:-${TZ:-UTC}}"
|
| 31 |
export TZ="${TZ:-$GENERIC_TIMEZONE}"
|
| 32 |
|
| 33 |
-
#
|
| 34 |
-
export
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
if [ -z "${N8N_BASIC_AUTH_PASSWORD:-}" ]; then
|
| 39 |
-
echo "β οΈ WARNING: N8N_BASIC_AUTH_ACTIVE=true but N8N_BASIC_AUTH_PASSWORD is not set."
|
| 40 |
-
echo " Your n8n instance is NOT protected. Set N8N_BASIC_AUTH_PASSWORD in Secrets."
|
| 41 |
-
fi
|
| 42 |
-
fi
|
| 43 |
|
| 44 |
echo ""
|
| 45 |
echo " ββββββββββββββββββββββββββββββββββββββ"
|
|
@@ -61,15 +58,9 @@ fi
|
|
| 61 |
|
| 62 |
cleanup() {
|
| 63 |
echo "Stopping Hugging8n..."
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
if [ -n "${N8N_PID:-}" ] && kill -0 "$N8N_PID" 2>/dev/null; then
|
| 68 |
-
kill "$N8N_PID" 2>/dev/null || true
|
| 69 |
-
fi
|
| 70 |
-
if [ -n "${PROXY_PID:-}" ] && kill -0 "$PROXY_PID" 2>/dev/null; then
|
| 71 |
-
kill "$PROXY_PID" 2>/dev/null || true
|
| 72 |
-
fi
|
| 73 |
if [ -n "${HF_TOKEN:-}" ]; then
|
| 74 |
echo "Running final backup pass..."
|
| 75 |
python3 "$APP_DIR/n8n-sync.py" sync-once || true
|
|
@@ -89,4 +80,11 @@ PROXY_PID=$!
|
|
| 89 |
n8n start &
|
| 90 |
N8N_PID=$!
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
wait "$N8N_PID"
|
|
|
|
| 1 |
#!/bin/bash
|
| 2 |
set -euo pipefail
|
| 3 |
|
| 4 |
+
# Tighten default file permissions for any files created by this process
|
| 5 |
+
umask 0077
|
| 6 |
+
|
| 7 |
APP_DIR="/home/node/app"
|
| 8 |
N8N_HOME="/home/node/.n8n"
|
| 9 |
N8N_PORT="${N8N_PORT:-5678}"
|
|
|
|
| 15 |
SPACE_HOST_DETECTED="${SPACE_HOST_OVERRIDE:-${SPACE_HOST:-}}"
|
| 16 |
if [ -n "$SPACE_HOST_DETECTED" ]; then
|
| 17 |
export N8N_HOST="${N8N_HOST:-$SPACE_HOST_DETECTED}"
|
| 18 |
+
# Updated for /app base path
|
| 19 |
+
export WEBHOOK_URL="${WEBHOOK_URL:-https://${SPACE_HOST_DETECTED}/app/}"
|
| 20 |
+
export N8N_EDITOR_BASE_URL="${N8N_EDITOR_BASE_URL:-https://${SPACE_HOST_DETECTED}/app/}"
|
| 21 |
fi
|
| 22 |
|
| 23 |
export N8N_PORT
|
| 24 |
export N8N_PROTOCOL="${N8N_PROTOCOL:-https}"
|
| 25 |
export N8N_PROXY_HOPS="${N8N_PROXY_HOPS:-1}"
|
| 26 |
export N8N_LISTEN_ADDRESS="${N8N_LISTEN_ADDRESS:-0.0.0.0}"
|
|
|
|
|
|
|
| 27 |
export N8N_SECURE_COOKIE="${N8N_SECURE_COOKIE:-false}"
|
| 28 |
export N8N_DIAGNOSTICS_ENABLED="${N8N_DIAGNOSTICS_ENABLED:-false}"
|
| 29 |
export N8N_PERSONALIZATION_ENABLED="${N8N_PERSONALIZATION_ENABLED:-false}"
|
|
|
|
| 32 |
export GENERIC_TIMEZONE="${GENERIC_TIMEZONE:-${TZ:-UTC}}"
|
| 33 |
export TZ="${TZ:-$GENERIC_TIMEZONE}"
|
| 34 |
|
| 35 |
+
# Disable noisy or unnecessary services
|
| 36 |
+
export N8N_RUNNERS_ENABLED="${N8N_RUNNERS_ENABLED:-false}"
|
| 37 |
+
export N8N_LICENSE_AUTO_RENEW_ENABLED="${N8N_LICENSE_AUTO_RENEW_ENABLED:-false}"
|
| 38 |
+
|
| 39 |
+
# n8n v2 uses built-in user management.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
echo ""
|
| 42 |
echo " ββββββββββββββββββββββββββββββββββββββ"
|
|
|
|
| 58 |
|
| 59 |
cleanup() {
|
| 60 |
echo "Stopping Hugging8n..."
|
| 61 |
+
[ -n "${SYNC_PID:-}" ] && kill "$SYNC_PID" 2>/dev/null || true
|
| 62 |
+
[ -n "${N8N_PID:-}" ] && kill "$N8N_PID" 2>/dev/null || true
|
| 63 |
+
[ -n "${PROXY_PID:-}" ] && kill "$PROXY_PID" 2>/dev/null || true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
if [ -n "${HF_TOKEN:-}" ]; then
|
| 65 |
echo "Running final backup pass..."
|
| 66 |
python3 "$APP_DIR/n8n-sync.py" sync-once || true
|
|
|
|
| 80 |
n8n start &
|
| 81 |
N8N_PID=$!
|
| 82 |
|
| 83 |
+
# Readiness probe
|
| 84 |
+
echo "Waiting for n8n to be ready on port ${N8N_PORT}..."
|
| 85 |
+
until curl -sf "http://127.0.0.1:${N8N_PORT}/healthz" > /dev/null 2>&1; do
|
| 86 |
+
sleep 1
|
| 87 |
+
done
|
| 88 |
+
echo "n8n is ready!"
|
| 89 |
+
|
| 90 |
wait "$N8N_PID"
|