@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 CHANGED
@@ -122,15 +122,59 @@ console.log(`Share this link: ${publicUrl}`);
122
122
 
123
123
  ### How Secrets Are Loaded
124
124
 
125
- Secrets are resolved in **priority order** (first match wins):
125
+ As of v1.3.0, secrets are resolved using a **3-tier vault pattern** with fallback:
126
126
 
127
- 1. **Explicit constructor config** values you pass directly
128
- 2. **Environment variables** — `process.env.*`
129
- 3. **`.env` file** automatically found by walking up parent directories from `process.cwd()`
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 `.env` loader supports monorepo layouts: if your `.env` is at the workspace root and the package runs from `packages/file-sharing/`, it will find it automatically.
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
- ### Environment Variables
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` (via gateway on :443) or `tcp` (direct) | `wss` |
145
- | `KADI_TUNNEL_WSS_HOST` | WSS gateway hostname (e.g., `tunnel-control.kadi.build`) | — |
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.2.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",
@@ -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
- * Parse a .env file's content and inject into process.env.
25
- * Existing env vars take priority (are NOT overwritten).
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
- // Real env vars take priority over .env file values
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
- * Walk up from cwd looking for a .env file (supports monorepo layouts
50
- * where .env lives at the workspace root, not inside packages/*).
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 _loadEnvFile() {
54
- let dir = process.cwd();
55
- const root = path.parse(dir).root;
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
- while (true) {
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 envPath = path.join(dir, '.env');
60
- const content = fs.readFileSync(envPath, 'utf8');
61
- _parseEnvFile(content);
62
- return; // found and loaded — done
63
- } catch {
64
- // not found in this directory — keep climbing
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
- const parent = path.dirname(dir);
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 from environment variables and optional .env file.
74
- * Supports KADI_ prefixed vars and common tunnel/auth env vars.
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
- * @returns {object} Secrets object (values are strings or undefined)
175
+ * @param {object} [kadiClient]
176
+ * @returns {Promise<object>}
94
177
  */
95
- function loadSecrets() {
96
- // Find and load .env file walk up parent directories (monorepo support).
97
- // e.g. packages/file-sharing/ packages/ → workspace root (where .env lives)
98
- _loadEnvFile();
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: process.env.KADI_TUNNEL_TOKEN,
103
- kadiServer: process.env.KADI_TUNNEL_SERVER,
104
- kadiDomain: process.env.KADI_TUNNEL_DOMAIN,
105
- kadiPort: process.env.KADI_TUNNEL_PORT ? Number(process.env.KADI_TUNNEL_PORT) : undefined,
106
- kadiSshPort: process.env.KADI_TUNNEL_SSH_PORT ? Number(process.env.KADI_TUNNEL_SSH_PORT) : undefined,
107
- kadiMode: process.env.KADI_TUNNEL_MODE,
108
- kadiAgentId: process.env.KADI_AGENT_ID,
109
- kadiTransport: process.env.KADI_TUNNEL_TRANSPORT,
110
- kadiWssControlHost: process.env.KADI_TUNNEL_WSS_HOST,
111
- ngrokAuthToken: process.env.NGROK_AUTHTOKEN || process.env.NGROK_AUTH_TOKEN,
112
- s3AccessKey: process.env.KADI_S3_ACCESS_KEY,
113
- s3SecretKey: process.env.KADI_S3_SECRET_KEY,
114
- authUsername: process.env.KADI_AUTH_USERNAME,
115
- authPassword: process.env.KADI_AUTH_PASSWORD,
116
- authApiKey: process.env.KADI_AUTH_API_KEY
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
- // Load secrets from env vars / .env (constructor config takes priority)
206
- // ----------------------------------------------------------------
207
- const secrets = loadSecrets();
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 vars > null
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
- // Initialize components
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
- this.s3Server = null;
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 env vars + explicit tunnel config.
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
- const tunnelManagerConfig = _filterDefined({
248
- primaryService: tunnelCfg.service || 'kadi',
249
- autoFallback: tunnelCfg.autoFallback,
250
- // KĀDI auth (env vars as fallback)
251
- kadiToken: tunnelCfg.kadiToken || secrets.kadiToken,
252
- kadiServer: tunnelCfg.kadiServer || secrets.kadiServer,
253
- kadiDomain: tunnelCfg.kadiDomain || secrets.kadiDomain,
254
- kadiPort: tunnelCfg.kadiPort || secrets.kadiPort,
255
- kadiSshPort: tunnelCfg.kadiSshPort || secrets.kadiSshPort,
256
- kadiMode: tunnelCfg.kadiMode || secrets.kadiMode,
257
- kadiAgentId: tunnelCfg.kadiAgentId || secrets.kadiAgentId,
258
- // KĀDI transport (WSS gateway)
259
- kadiTransport: tunnelCfg.kadiTransport || secrets.kadiTransport,
260
- kadiWssControlHost: tunnelCfg.kadiWssControlHost || secrets.kadiWssControlHost,
261
- // Ngrok auth (env vars as fallback)
262
- ngrokAuthToken: tunnelCfg.ngrokAuthToken || secrets.ngrokAuthToken,
263
- // Pass through any extra service-specific options
264
- ...(tunnelCfg.managerOptions || {})
265
- });
266
- this.tunnelManager = new TunnelManager(tunnelManagerConfig);
267
- this.downloadMonitor = new DownloadMonitor();
268
- this.shutdownManager = new ShutdownManager(this.config.shutdown);
269
- this.monitoringDashboard = new MonitoringDashboard();
270
- this.eventNotifier = new EventNotifier();
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
- // State
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