@kadi.build/file-sharing 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -33
- package/package.json +2 -1
- package/src/FileSharingServer.js +235 -102
package/README.md
CHANGED
|
@@ -122,15 +122,59 @@ console.log(`Share this link: ${publicUrl}`);
|
|
|
122
122
|
|
|
123
123
|
### How Secrets Are Loaded
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
As of v1.3.0, secrets are resolved using a **3-tier vault pattern** with fallback:
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
| Priority | Source | What it provides |
|
|
128
|
+
|----------|--------|------------------|
|
|
129
|
+
| 1 (highest) | `process.env` | Direct overrides at startup |
|
|
130
|
+
| 2 | **Constructor config** | Values passed to `FileSharingServer` |
|
|
131
|
+
| 3 | **Vault** (`secrets.toml`) | Encrypted tokens via `secret-ability` |
|
|
132
|
+
| 4 | **`config.yml`** | Non-secret settings (walks up from CWD) |
|
|
133
|
+
| 5 (fallback) | **`.env` file** | Legacy fallback (walks up from CWD) |
|
|
130
134
|
|
|
131
|
-
The
|
|
135
|
+
The caller (typically `deploy-ability` or `kadi-deploy`) must have `secret-ability` installed via `kadi install` for vault access. If no vault is found, the system gracefully falls back to `.env` files.
|
|
132
136
|
|
|
133
|
-
###
|
|
137
|
+
### Vault Setup
|
|
138
|
+
|
|
139
|
+
Store tunnel tokens in the `tunnel` vault and file-sharing secrets in the `file-sharing` vault:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Tunnel tokens (shared with tunnel-services)
|
|
143
|
+
kadi secret set -v tunnel KADI_TUNNEL_TOKEN <your-token>
|
|
144
|
+
kadi secret set -v tunnel NGROK_AUTH_TOKEN <your-token>
|
|
145
|
+
|
|
146
|
+
# File-sharing secrets (optional)
|
|
147
|
+
kadi secret set -v file-sharing KADI_AUTH_API_KEY <your-api-key>
|
|
148
|
+
kadi secret set -v file-sharing KADI_S3_ACCESS_KEY <your-key>
|
|
149
|
+
kadi secret set -v file-sharing KADI_S3_SECRET_KEY <your-secret>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### config.yml
|
|
153
|
+
|
|
154
|
+
Place a `config.yml` in your project root (or any parent directory):
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
# Tunnel config (shared across tunnel-services consumers)
|
|
158
|
+
tunnel:
|
|
159
|
+
server_addr: broker.kadi.build
|
|
160
|
+
tunnel_domain: tunnel.kadi.build
|
|
161
|
+
server_port: 7000
|
|
162
|
+
ssh_port: 2200
|
|
163
|
+
mode: frpc
|
|
164
|
+
transport: wss
|
|
165
|
+
wss_control_host: tunnel-control.kadi.build
|
|
166
|
+
agent_id: kadi
|
|
167
|
+
|
|
168
|
+
# File-sharing specific config
|
|
169
|
+
file-sharing:
|
|
170
|
+
port: 3000
|
|
171
|
+
host: 0.0.0.0
|
|
172
|
+
enable_directory_listing: true
|
|
173
|
+
cors: true
|
|
174
|
+
enable_s3: false
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Environment Variables (overrides)
|
|
134
178
|
|
|
135
179
|
| Variable | Description | Default |
|
|
136
180
|
|----------|-------------|---------|
|
|
@@ -141,8 +185,8 @@ The `.env` loader supports monorepo layouts: if your `.env` is at the workspace
|
|
|
141
185
|
| `KADI_TUNNEL_PORT` | KĀDI frps server port | `7000` |
|
|
142
186
|
| `KADI_TUNNEL_SSH_PORT` | KĀDI SSH gateway port | `2200` |
|
|
143
187
|
| `KADI_TUNNEL_MODE` | Connection mode: `ssh`, `frpc`, or `auto` | `auto` |
|
|
144
|
-
| `KADI_TUNNEL_TRANSPORT` | Transport protocol: `wss`
|
|
145
|
-
| `KADI_TUNNEL_WSS_HOST` | WSS gateway hostname
|
|
188
|
+
| `KADI_TUNNEL_TRANSPORT` | Transport protocol: `wss` or `tcp` | `wss` |
|
|
189
|
+
| `KADI_TUNNEL_WSS_HOST` | WSS gateway hostname | — |
|
|
146
190
|
| `KADI_AGENT_ID` | Agent identifier for proxy naming | `kadi` |
|
|
147
191
|
| **Tunnel — Ngrok** | | |
|
|
148
192
|
| `NGROK_AUTHTOKEN` | Ngrok auth token (also accepts `NGROK_AUTH_TOKEN`) | — |
|
|
@@ -154,31 +198,6 @@ The `.env` loader supports monorepo layouts: if your `.env` is at the workspace
|
|
|
154
198
|
| `KADI_S3_ACCESS_KEY` | S3 access key ID | `minioadmin` |
|
|
155
199
|
| `KADI_S3_SECRET_KEY` | S3 secret access key | `minioadmin` |
|
|
156
200
|
|
|
157
|
-
### `.env` File Example
|
|
158
|
-
|
|
159
|
-
```env
|
|
160
|
-
# Tunnel credentials
|
|
161
|
-
KADI_TUNNEL_TOKEN=your-kadi-token-here
|
|
162
|
-
KADI_TUNNEL_SERVER=broker.kadi.build
|
|
163
|
-
KADI_TUNNEL_DOMAIN=tunnel.kadi.build
|
|
164
|
-
KADI_TUNNEL_PORT=7000
|
|
165
|
-
KADI_TUNNEL_SSH_PORT=2200
|
|
166
|
-
KADI_TUNNEL_MODE=ssh
|
|
167
|
-
KADI_TUNNEL_TRANSPORT=wss
|
|
168
|
-
KADI_TUNNEL_WSS_HOST=tunnel-control.kadi.build
|
|
169
|
-
KADI_AGENT_ID=my-agent
|
|
170
|
-
|
|
171
|
-
# Optional: Ngrok (fallback)
|
|
172
|
-
NGROK_AUTHTOKEN=your-ngrok-token
|
|
173
|
-
|
|
174
|
-
# HTTP auth (optional — protects file downloads)
|
|
175
|
-
KADI_AUTH_API_KEY=my-secret-api-key
|
|
176
|
-
|
|
177
|
-
# S3 credentials (optional — default minioadmin/minioadmin)
|
|
178
|
-
KADI_S3_ACCESS_KEY=my-access-key
|
|
179
|
-
KADI_S3_SECRET_KEY=my-secret-key
|
|
180
|
-
```
|
|
181
|
-
|
|
182
201
|
### HTTP Authentication Schemes
|
|
183
202
|
|
|
184
203
|
When `auth` is configured (via config, env vars, or `.env`), the HTTP server supports three authentication schemes:
|
|
@@ -584,6 +603,56 @@ Set `KADI_TUNNEL_TRANSPORT=wss` and `KADI_TUNNEL_WSS_HOST=tunnel-control.kadi.bu
|
|
|
584
603
|
|
|
585
604
|
If KĀDI credentials are not provided, the tunnel will automatically fall back to free services (serveo → localtunnel → pinggy → localhost.run) unless `autoFallback: false` is set.
|
|
586
605
|
|
|
606
|
+
### Container Usage — Installing `frpc`
|
|
607
|
+
|
|
608
|
+
When running inside a Docker container (e.g. via `kadi deploy`), the KĀDI tunnel requires the **frpc** binary to be available at build time. Alpine-based images (like `node:22-alpine`) do not include it by default.
|
|
609
|
+
|
|
610
|
+
Add the following lines to the `run` array in your `agent.json` build config:
|
|
611
|
+
|
|
612
|
+
```json
|
|
613
|
+
"run": [
|
|
614
|
+
"apk add --no-cache curl openssh-client",
|
|
615
|
+
"FRPC_VERSION=0.61.1 && wget -qO /tmp/frpc.tar.gz https://github.com/fatedier/frp/releases/download/v${FRPC_VERSION}/frp_${FRPC_VERSION}_linux_amd64.tar.gz && tar -xzf /tmp/frpc.tar.gz -C /tmp && mv /tmp/frp_${FRPC_VERSION}_linux_amd64/frpc /usr/local/bin/frpc && chmod +x /usr/local/bin/frpc && rm -rf /tmp/frpc.tar.gz /tmp/frp_${FRPC_VERSION}_linux_amd64"
|
|
616
|
+
]
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**What these lines do:**
|
|
620
|
+
|
|
621
|
+
1. **`apk add --no-cache curl openssh-client`** — Installs `curl` and the SSH client (needed for SSH-mode fallback).
|
|
622
|
+
2. **`FRPC_VERSION=0.61.1 && wget …`** — Downloads the frpc binary (v0.61.1) from the official [fatedier/frp](https://github.com/fatedier/frp) releases, extracts it to `/usr/local/bin/frpc`, and cleans up temp files.
|
|
623
|
+
|
|
624
|
+
> **Note:** For non-Alpine images (Debian/Ubuntu-based), replace `apk add` with `apt-get update && apt-get install -y curl openssh-client`.
|
|
625
|
+
|
|
626
|
+
See [`arcadedb-ability/agent.json`](../arcadedb-ability/agent.json) and [`backup-ability/agent.json`](../backup-ability/agent.json) for real-world examples.
|
|
627
|
+
|
|
628
|
+
### Troubleshooting Tunnels
|
|
629
|
+
|
|
630
|
+
If your deployed container fails to establish a tunnel, the most common cause is a **missing `frpc` binary**. Symptoms include:
|
|
631
|
+
|
|
632
|
+
- Tunnel creation silently fails or times out
|
|
633
|
+
- Logs show `frpc: not found` or the service falls back to SSH mode unexpectedly
|
|
634
|
+
- The agent connects to the broker but is not reachable via its tunnel URL
|
|
635
|
+
|
|
636
|
+
**Fix:** Ensure the frpc installation lines above are in your `agent.json` `build.*.run` array, then rebuild:
|
|
637
|
+
|
|
638
|
+
```bash
|
|
639
|
+
kadi build
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Verify frpc is installed inside the container:
|
|
643
|
+
|
|
644
|
+
```bash
|
|
645
|
+
frpc --version
|
|
646
|
+
# Expected: frpc version 0.61.1
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
| Symptom | Likely Cause | Fix |
|
|
650
|
+
|---------|-------------|-----|
|
|
651
|
+
| `frpc: not found` | frpc not installed in the container image | Add the `run` lines above and rebuild |
|
|
652
|
+
| Tunnel times out | Network/firewall blocking port 7000 | Set `KADI_TUNNEL_TRANSPORT=wss` to use port 443 |
|
|
653
|
+
| SSH fallback instead of frpc | frpc binary missing from `$PATH` | Install frpc (see above) |
|
|
654
|
+
| `Permission denied` on frpc | Binary not executable | Ensure `chmod +x /usr/local/bin/frpc` is in your build |
|
|
655
|
+
|
|
587
656
|
---
|
|
588
657
|
|
|
589
658
|
## Dependencies
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kadi.build/file-sharing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "File sharing service with tunneling and local S3-compatible interface",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@kadi.build/file-manager": "^1.0.0",
|
|
49
49
|
"@kadi.build/tunnel-services": "^1.0.5",
|
|
50
|
+
"js-yaml": "^4.1.0",
|
|
50
51
|
"chalk": "^5.3.0",
|
|
51
52
|
"cors": "^2.8.5",
|
|
52
53
|
"express": "^4.18.0",
|
package/src/FileSharingServer.js
CHANGED
|
@@ -6,13 +6,20 @@
|
|
|
6
6
|
* file sharing solution.
|
|
7
7
|
*
|
|
8
8
|
* KĀDI is the default tunnel service.
|
|
9
|
+
*
|
|
10
|
+
* Configuration resolution:
|
|
11
|
+
* 1. config.yml walk-up → "tunnel" section (non-secret settings)
|
|
12
|
+
* + "file-sharing" section (server-specific settings)
|
|
13
|
+
* 2. secrets.toml vault → "tunnel" vault for tokens (via secret-ability)
|
|
14
|
+
* 3. .env walk-up → fallback when no vault found
|
|
15
|
+
* 4. process.env → always wins (direct environment overrides)
|
|
9
16
|
*/
|
|
10
17
|
|
|
11
18
|
import { EventEmitter } from 'events';
|
|
12
19
|
import fs from 'fs';
|
|
13
20
|
import path from 'path';
|
|
14
21
|
import { createFileManager } from '@kadi.build/file-manager';
|
|
15
|
-
import { TunnelManager } from '@kadi.build/tunnel-services';
|
|
22
|
+
import { TunnelManager, resolveTunnelConfig, buildTunnelManagerConfig } from '@kadi.build/tunnel-services';
|
|
16
23
|
import { HttpServerProvider } from './HttpServerProvider.js';
|
|
17
24
|
import { S3Server } from './S3Server.js';
|
|
18
25
|
import { DownloadMonitor } from './DownloadMonitor.js';
|
|
@@ -20,12 +27,41 @@ import { ShutdownManager } from './ShutdownManager.js';
|
|
|
20
27
|
import { MonitoringDashboard } from './MonitoringDashboard.js';
|
|
21
28
|
import { EventNotifier } from './EventNotifier.js';
|
|
22
29
|
|
|
30
|
+
let yaml;
|
|
31
|
+
try {
|
|
32
|
+
const m = await import('js-yaml');
|
|
33
|
+
yaml = m.default || m;
|
|
34
|
+
} catch {
|
|
35
|
+
yaml = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Walk-up helpers ──────────────────────────────────────────────────
|
|
39
|
+
|
|
23
40
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
41
|
+
* Walk up from startDir looking for a filename.
|
|
42
|
+
* @param {string} filename
|
|
43
|
+
* @param {string} [startDir]
|
|
44
|
+
* @returns {string|null}
|
|
45
|
+
*/
|
|
46
|
+
function _walkUpFind(filename, startDir) {
|
|
47
|
+
let dir = startDir || process.cwd();
|
|
48
|
+
while (true) {
|
|
49
|
+
const candidate = path.join(dir, filename);
|
|
50
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
51
|
+
const parent = path.dirname(dir);
|
|
52
|
+
if (parent === dir) break;
|
|
53
|
+
dir = parent;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse a .env file's content into an object (does NOT inject into process.env).
|
|
26
60
|
* @param {string} content raw file content
|
|
61
|
+
* @returns {Record<string, string>}
|
|
27
62
|
*/
|
|
28
63
|
function _parseEnvFile(content) {
|
|
64
|
+
const result = {};
|
|
29
65
|
for (const line of content.split('\n')) {
|
|
30
66
|
const trimmed = line.trim();
|
|
31
67
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
@@ -38,82 +74,137 @@ function _parseEnvFile(content) {
|
|
|
38
74
|
(val.startsWith("'") && val.endsWith("'"))) {
|
|
39
75
|
val = val.slice(1, -1);
|
|
40
76
|
}
|
|
41
|
-
|
|
42
|
-
if (process.env[key] === undefined) {
|
|
43
|
-
process.env[key] = val;
|
|
44
|
-
}
|
|
77
|
+
result[key] = val;
|
|
45
78
|
}
|
|
79
|
+
return result;
|
|
46
80
|
}
|
|
47
81
|
|
|
48
82
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* Stops at the filesystem root.
|
|
83
|
+
* Load file-sharing specific config from config.yml "file-sharing" section.
|
|
84
|
+
* @returns {Record<string, any>}
|
|
52
85
|
*/
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
const
|
|
86
|
+
function _loadFileSharingConfig() {
|
|
87
|
+
if (!yaml) return {};
|
|
88
|
+
const configPath = _walkUpFind('config.yml');
|
|
89
|
+
if (!configPath) return {};
|
|
90
|
+
try {
|
|
91
|
+
const parsed = yaml.load(fs.readFileSync(configPath, 'utf8'));
|
|
92
|
+
return (parsed && parsed['file-sharing']) || {};
|
|
93
|
+
} catch {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
56
97
|
|
|
57
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Load file-sharing secrets from vault or .env fallback.
|
|
100
|
+
*
|
|
101
|
+
* Secret keys for file-sharing (S3 credentials, auth):
|
|
102
|
+
* vault "file-sharing" (or configured vault):
|
|
103
|
+
* KADI_S3_ACCESS_KEY, KADI_S3_SECRET_KEY
|
|
104
|
+
* KADI_AUTH_USERNAME, KADI_AUTH_PASSWORD, KADI_AUTH_API_KEY
|
|
105
|
+
*
|
|
106
|
+
* Tunnel secrets are handled by resolveTunnelConfig() separately.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} [kadiClient]
|
|
109
|
+
* @returns {Promise<Record<string, string>>}
|
|
110
|
+
*/
|
|
111
|
+
async function _loadFileSharingSecrets(kadiClient) {
|
|
112
|
+
const { execSync } = await import('child_process');
|
|
113
|
+
const VAULT_NAME = 'file-sharing';
|
|
114
|
+
const KEYS = [
|
|
115
|
+
'KADI_S3_ACCESS_KEY', 'KADI_S3_SECRET_KEY',
|
|
116
|
+
'KADI_AUTH_USERNAME', 'KADI_AUTH_PASSWORD', 'KADI_AUTH_API_KEY',
|
|
117
|
+
];
|
|
118
|
+
const secrets = {};
|
|
119
|
+
|
|
120
|
+
// Tier 1 — native secret-ability
|
|
121
|
+
if (kadiClient) {
|
|
58
122
|
try {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
123
|
+
const sa = await kadiClient.loadNative('secret-ability');
|
|
124
|
+
try {
|
|
125
|
+
for (const key of KEYS) {
|
|
126
|
+
try {
|
|
127
|
+
const r = await sa.invoke('get', { vault: VAULT_NAME, key });
|
|
128
|
+
if (r && r.value) secrets[key] = r.value;
|
|
129
|
+
} catch { /* skip */ }
|
|
130
|
+
}
|
|
131
|
+
} finally {
|
|
132
|
+
try { await sa.disconnect(); } catch { /* ignore */ }
|
|
133
|
+
}
|
|
134
|
+
if (Object.keys(secrets).length > 0) return secrets;
|
|
135
|
+
} catch { /* fall through */ }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Tier 2 — kadi CLI
|
|
139
|
+
let cliAvailable;
|
|
140
|
+
try {
|
|
141
|
+
execSync('kadi --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
142
|
+
cliAvailable = true;
|
|
143
|
+
} catch {
|
|
144
|
+
cliAvailable = false;
|
|
145
|
+
}
|
|
146
|
+
if (cliAvailable) {
|
|
147
|
+
for (const key of KEYS) {
|
|
148
|
+
try {
|
|
149
|
+
const val = execSync(`kadi secret get -v ${VAULT_NAME} ${key}`, {
|
|
150
|
+
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
151
|
+
}).trim();
|
|
152
|
+
if (val) secrets[key] = val;
|
|
153
|
+
} catch { /* skip */ }
|
|
65
154
|
}
|
|
66
|
-
|
|
67
|
-
if (parent === dir || dir === root) break; // reached fs root
|
|
68
|
-
dir = parent;
|
|
155
|
+
if (Object.keys(secrets).length > 0) return secrets;
|
|
69
156
|
}
|
|
157
|
+
|
|
158
|
+
// Tier 3 — .env fallback
|
|
159
|
+
const envPath = _walkUpFind('.env');
|
|
160
|
+
if (envPath) {
|
|
161
|
+
const parsed = _parseEnvFile(fs.readFileSync(envPath, 'utf8'));
|
|
162
|
+
for (const key of KEYS) {
|
|
163
|
+
if (parsed[key]) secrets[key] = parsed[key];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return secrets;
|
|
70
168
|
}
|
|
71
169
|
|
|
72
170
|
/**
|
|
73
|
-
* Load secrets
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* Env vars (all optional — constructor config takes priority):
|
|
77
|
-
* KADI_TUNNEL_TOKEN — KĀDI tunnel auth token
|
|
78
|
-
* KADI_TUNNEL_SERVER — KĀDI broker address (default: broker.kadi.build)
|
|
79
|
-
* KADI_TUNNEL_DOMAIN — KĀDI tunnel domain (default: tunnel.kadi.build)
|
|
80
|
-
* KADI_TUNNEL_PORT — KĀDI frpc port (default: 7000)
|
|
81
|
-
* KADI_TUNNEL_SSH_PORT— KĀDI SSH gateway port (default: 2200)
|
|
82
|
-
* KADI_TUNNEL_MODE — ssh | frpc | auto (default: auto)
|
|
83
|
-
* KADI_TUNNEL_TRANSPORT— wss | tcp (default: wss)
|
|
84
|
-
* KADI_TUNNEL_WSS_HOST — WSS gateway hostname (e.g., tunnel-control.kadi.build)
|
|
85
|
-
* KADI_AGENT_ID — Agent identifier for proxy naming
|
|
86
|
-
* NGROK_AUTHTOKEN — Ngrok auth token (also: NGROK_AUTH_TOKEN)
|
|
87
|
-
* KADI_S3_ACCESS_KEY — S3 access key (default: minioadmin)
|
|
88
|
-
* KADI_S3_SECRET_KEY — S3 secret key (default: minioadmin)
|
|
89
|
-
* KADI_AUTH_USERNAME — HTTP Basic auth username
|
|
90
|
-
* KADI_AUTH_PASSWORD — HTTP Basic auth password
|
|
91
|
-
* KADI_AUTH_API_KEY — API key for Bearer/X-API-Key auth
|
|
171
|
+
* Load ALL secrets needed by FileSharingServer.
|
|
172
|
+
* Merges tunnel secrets (from tunnel config resolver) + file-sharing specific secrets.
|
|
173
|
+
* process.env overrides always win.
|
|
92
174
|
*
|
|
93
|
-
* @
|
|
175
|
+
* @param {object} [kadiClient]
|
|
176
|
+
* @returns {Promise<object>}
|
|
94
177
|
*/
|
|
95
|
-
function loadSecrets() {
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
178
|
+
async function loadSecrets(kadiClient) {
|
|
179
|
+
// Resolve tunnel config + secrets via tunnel-services configResolver
|
|
180
|
+
const tunnelResolved = await resolveTunnelConfig({ kadiClient });
|
|
181
|
+
const tunnelSecrets = tunnelResolved.secrets;
|
|
99
182
|
|
|
183
|
+
// Load file-sharing specific secrets (S3, auth)
|
|
184
|
+
const fsSecrets = await _loadFileSharingSecrets(kadiClient);
|
|
100
185
|
|
|
186
|
+
// process.env overrides (always win)
|
|
187
|
+
const env = process.env;
|
|
101
188
|
return {
|
|
102
|
-
kadiToken:
|
|
103
|
-
kadiServer:
|
|
104
|
-
kadiDomain:
|
|
105
|
-
kadiPort:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
189
|
+
kadiToken: env.KADI_TUNNEL_TOKEN || tunnelSecrets.kadi_token,
|
|
190
|
+
kadiServer: env.KADI_TUNNEL_SERVER || tunnelResolved.config.server_addr,
|
|
191
|
+
kadiDomain: env.KADI_TUNNEL_DOMAIN || tunnelResolved.config.tunnel_domain,
|
|
192
|
+
kadiPort: env.KADI_TUNNEL_PORT ? Number(env.KADI_TUNNEL_PORT) :
|
|
193
|
+
tunnelResolved.config.server_port,
|
|
194
|
+
kadiSshPort: env.KADI_TUNNEL_SSH_PORT ? Number(env.KADI_TUNNEL_SSH_PORT) :
|
|
195
|
+
tunnelResolved.config.ssh_port,
|
|
196
|
+
kadiMode: env.KADI_TUNNEL_MODE || tunnelResolved.config.mode,
|
|
197
|
+
kadiAgentId: env.KADI_AGENT_ID || tunnelResolved.config.agent_id,
|
|
198
|
+
kadiTransport: env.KADI_TUNNEL_TRANSPORT || tunnelResolved.config.transport,
|
|
199
|
+
kadiWssControlHost: env.KADI_TUNNEL_WSS_HOST || tunnelResolved.config.wss_control_host,
|
|
200
|
+
ngrokAuthToken: env.NGROK_AUTHTOKEN || env.NGROK_AUTH_TOKEN || tunnelSecrets.ngrok_token,
|
|
201
|
+
s3AccessKey: env.KADI_S3_ACCESS_KEY || fsSecrets.KADI_S3_ACCESS_KEY,
|
|
202
|
+
s3SecretKey: env.KADI_S3_SECRET_KEY || fsSecrets.KADI_S3_SECRET_KEY,
|
|
203
|
+
authUsername: env.KADI_AUTH_USERNAME || fsSecrets.KADI_AUTH_USERNAME,
|
|
204
|
+
authPassword: env.KADI_AUTH_PASSWORD || fsSecrets.KADI_AUTH_PASSWORD,
|
|
205
|
+
authApiKey: env.KADI_AUTH_API_KEY || fsSecrets.KADI_AUTH_API_KEY,
|
|
206
|
+
// Pass through for TunnelManager config building
|
|
207
|
+
_tunnelResolved: tunnelResolved,
|
|
117
208
|
};
|
|
118
209
|
}
|
|
119
210
|
|
|
@@ -138,9 +229,15 @@ function _clientHost(host) {
|
|
|
138
229
|
}
|
|
139
230
|
|
|
140
231
|
export class FileSharingServer extends EventEmitter {
|
|
232
|
+
/**
|
|
233
|
+
* @param {object} [config]
|
|
234
|
+
* @param {object} [config.kadiClient] KadiClient for native vault access
|
|
235
|
+
*/
|
|
141
236
|
constructor(config = {}) {
|
|
142
237
|
super();
|
|
143
238
|
|
|
239
|
+
this._kadiClient = config.kadiClient || null;
|
|
240
|
+
|
|
144
241
|
this.config = {
|
|
145
242
|
staticDir: process.cwd(),
|
|
146
243
|
port: 3000,
|
|
@@ -201,12 +298,35 @@ export class FileSharingServer extends EventEmitter {
|
|
|
201
298
|
this.config.s3Port = 0;
|
|
202
299
|
}
|
|
203
300
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
|
|
301
|
+
// Initialize components (secrets resolved lazily in start())
|
|
302
|
+
this.fileManager = createFileManager();
|
|
303
|
+
this.httpServer = null; // Initialized in _initSecrets()
|
|
304
|
+
this.s3Server = null;
|
|
305
|
+
this.tunnelManager = null;
|
|
306
|
+
|
|
307
|
+
this.downloadMonitor = new DownloadMonitor();
|
|
308
|
+
this.shutdownManager = new ShutdownManager(this.config.shutdown);
|
|
309
|
+
this.monitoringDashboard = new MonitoringDashboard();
|
|
310
|
+
this.eventNotifier = new EventNotifier();
|
|
311
|
+
|
|
312
|
+
// State
|
|
313
|
+
this.isRunning = false;
|
|
314
|
+
this._secretsResolved = false;
|
|
315
|
+
this.tunnel = null;
|
|
316
|
+
this._startTime = null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Resolve secrets (vault → .env → process.env) and initialize components
|
|
321
|
+
* that depend on secret values. Called automatically from start().
|
|
322
|
+
*/
|
|
323
|
+
async _initSecrets() {
|
|
324
|
+
if (this._secretsResolved) return;
|
|
325
|
+
|
|
326
|
+
// ── Load secrets (config.yml + vault + .env + process.env) ──
|
|
327
|
+
const secrets = await loadSecrets(this._kadiClient);
|
|
208
328
|
|
|
209
|
-
// Build auth config: explicit config > env
|
|
329
|
+
// Build auth config: explicit config > vault/env > null
|
|
210
330
|
if (!this.config.auth) {
|
|
211
331
|
if (secrets.authApiKey) {
|
|
212
332
|
this.config.auth = { apiKey: secrets.authApiKey };
|
|
@@ -215,9 +335,7 @@ export class FileSharingServer extends EventEmitter {
|
|
|
215
335
|
}
|
|
216
336
|
}
|
|
217
337
|
|
|
218
|
-
//
|
|
219
|
-
this.fileManager = createFileManager();
|
|
220
|
-
|
|
338
|
+
// HTTP server
|
|
221
339
|
this.httpServer = new HttpServerProvider({
|
|
222
340
|
port: this.config.port,
|
|
223
341
|
host: this.config.host,
|
|
@@ -228,7 +346,7 @@ export class FileSharingServer extends EventEmitter {
|
|
|
228
346
|
...(this.config.httpConfig || {})
|
|
229
347
|
});
|
|
230
348
|
|
|
231
|
-
|
|
349
|
+
// S3 server
|
|
232
350
|
if (this.config.enableS3) {
|
|
233
351
|
this.s3Server = new S3Server({
|
|
234
352
|
port: this.config.s3Port,
|
|
@@ -240,44 +358,54 @@ export class FileSharingServer extends EventEmitter {
|
|
|
240
358
|
});
|
|
241
359
|
}
|
|
242
360
|
|
|
243
|
-
// Build TunnelManager config from
|
|
244
|
-
// Use || to let explicit config override env, and filter out undefined
|
|
245
|
-
// values so TunnelManager's own defaults are preserved.
|
|
361
|
+
// Build TunnelManager config from tunnel resolver + explicit tunnel config.
|
|
246
362
|
const tunnelCfg = this.config.tunnel || {};
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
363
|
+
let tunnelManagerConfig;
|
|
364
|
+
|
|
365
|
+
if (secrets._tunnelResolved) {
|
|
366
|
+
// Use the tunnel config resolver output, with explicit overrides
|
|
367
|
+
tunnelManagerConfig = buildTunnelManagerConfig(secrets._tunnelResolved, _filterDefined({
|
|
368
|
+
primaryService: tunnelCfg.service,
|
|
369
|
+
autoFallback: tunnelCfg.autoFallback,
|
|
370
|
+
kadiToken: tunnelCfg.kadiToken,
|
|
371
|
+
kadiServer: tunnelCfg.kadiServer,
|
|
372
|
+
kadiDomain: tunnelCfg.kadiDomain,
|
|
373
|
+
kadiPort: tunnelCfg.kadiPort,
|
|
374
|
+
kadiSshPort: tunnelCfg.kadiSshPort,
|
|
375
|
+
kadiMode: tunnelCfg.kadiMode,
|
|
376
|
+
kadiAgentId: tunnelCfg.kadiAgentId,
|
|
377
|
+
kadiTransport: tunnelCfg.kadiTransport,
|
|
378
|
+
kadiWssControlHost: tunnelCfg.kadiWssControlHost,
|
|
379
|
+
ngrokAuthToken: tunnelCfg.ngrokAuthToken,
|
|
380
|
+
...(tunnelCfg.managerOptions || {})
|
|
381
|
+
}));
|
|
382
|
+
} else {
|
|
383
|
+
// Fallback: build from secrets directly (backward compat)
|
|
384
|
+
tunnelManagerConfig = _filterDefined({
|
|
385
|
+
primaryService: tunnelCfg.service || 'kadi',
|
|
386
|
+
autoFallback: tunnelCfg.autoFallback,
|
|
387
|
+
kadiToken: tunnelCfg.kadiToken || secrets.kadiToken,
|
|
388
|
+
kadiServer: tunnelCfg.kadiServer || secrets.kadiServer,
|
|
389
|
+
kadiDomain: tunnelCfg.kadiDomain || secrets.kadiDomain,
|
|
390
|
+
kadiPort: tunnelCfg.kadiPort || secrets.kadiPort,
|
|
391
|
+
kadiSshPort: tunnelCfg.kadiSshPort || secrets.kadiSshPort,
|
|
392
|
+
kadiMode: tunnelCfg.kadiMode || secrets.kadiMode,
|
|
393
|
+
kadiAgentId: tunnelCfg.kadiAgentId || secrets.kadiAgentId,
|
|
394
|
+
kadiTransport: tunnelCfg.kadiTransport || secrets.kadiTransport,
|
|
395
|
+
kadiWssControlHost: tunnelCfg.kadiWssControlHost || secrets.kadiWssControlHost,
|
|
396
|
+
ngrokAuthToken: tunnelCfg.ngrokAuthToken || secrets.ngrokAuthToken,
|
|
397
|
+
...(tunnelCfg.managerOptions || {})
|
|
398
|
+
});
|
|
399
|
+
}
|
|
271
400
|
|
|
272
|
-
|
|
273
|
-
this.isRunning = false;
|
|
274
|
-
this.tunnel = null;
|
|
275
|
-
this._startTime = null;
|
|
401
|
+
this.tunnelManager = new TunnelManager(tunnelManagerConfig);
|
|
276
402
|
|
|
277
403
|
// Wire up events
|
|
278
404
|
this._setupEventForwarding();
|
|
279
405
|
this._setupShutdownHandlers();
|
|
280
406
|
this._setupWebhooks();
|
|
407
|
+
|
|
408
|
+
this._secretsResolved = true;
|
|
281
409
|
}
|
|
282
410
|
|
|
283
411
|
/**
|
|
@@ -289,10 +417,15 @@ export class FileSharingServer extends EventEmitter {
|
|
|
289
417
|
return this.getInfo();
|
|
290
418
|
}
|
|
291
419
|
|
|
420
|
+
// Resolve secrets from vault/config.yml/.env (lazy init)
|
|
421
|
+
await this._initSecrets();
|
|
422
|
+
|
|
292
423
|
this._startTime = Date.now();
|
|
293
424
|
|
|
294
425
|
// Start HTTP server
|
|
295
426
|
const httpResult = await this.httpServer.start();
|
|
427
|
+
// Sync the actual bound port (important when port=0, i.e. OS-assigned)
|
|
428
|
+
this.config.port = httpResult.port;
|
|
296
429
|
this.emit('http:started', httpResult);
|
|
297
430
|
|
|
298
431
|
// Start S3 server if enabled
|