@fyresmith/hive-server 4.0.0 → 5.0.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/cli/env-file.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import dotenv from 'dotenv';
2
2
  import prompts from 'prompts';
3
+ import { randomBytes } from 'crypto';
3
4
  import { access, mkdir, readFile, writeFile } from 'fs/promises';
4
5
  import { existsSync } from 'fs';
5
6
  import { dirname } from 'path';
@@ -57,37 +58,19 @@ export async function writeEnvFile(envFile, values) {
57
58
  await writeFile(envFile, serializeEnv(normalizeEnv(values)), 'utf-8');
58
59
  }
59
60
 
60
- export function inferDomainFromRedirect(redirectUri) {
61
- if (!redirectUri) return null;
62
- try {
63
- const u = new URL(redirectUri);
64
- return u.host;
65
- } catch {
66
- return null;
67
- }
68
- }
69
-
70
- export function validateEnvValues(values) {
61
+ export function validateEnvValues(values, { requireVaultPath = true } = {}) {
71
62
  const issues = [];
72
- for (const key of REQUIRED_ENV_KEYS) {
63
+ const requiredKeys = requireVaultPath
64
+ ? REQUIRED_ENV_KEYS
65
+ : REQUIRED_ENV_KEYS.filter((key) => key !== 'VAULT_PATH');
66
+ for (const key of requiredKeys) {
73
67
  if (!String(values[key] ?? '').trim()) {
74
68
  issues.push(`Missing ${key}`);
75
69
  }
76
70
  }
77
71
 
78
72
  const port = parseInt(values.PORT ?? '', 10);
79
- const yjsPort = parseInt(values.YJS_PORT ?? '', 10);
80
73
  if (!Number.isInteger(port) || port <= 0) issues.push('PORT must be a positive integer');
81
- if (!Number.isInteger(yjsPort) || yjsPort <= 0) issues.push('YJS_PORT must be a positive integer');
82
-
83
- try {
84
- const uri = new URL(values.DISCORD_REDIRECT_URI ?? '');
85
- if (!/^https?:$/.test(uri.protocol)) {
86
- issues.push('DISCORD_REDIRECT_URI must use http or https');
87
- }
88
- } catch {
89
- issues.push('DISCORD_REDIRECT_URI must be a valid URL');
90
- }
91
74
 
92
75
  return issues;
93
76
  }
@@ -103,7 +86,14 @@ export async function ensureVaultPathReadable(pathValue) {
103
86
  }
104
87
 
105
88
  export async function promptForEnv({ envFile, existing, yes = false, preset = {} }) {
106
- const current = normalizeEnv({ ...existing, ...preset });
89
+ const base = { ...existing, ...preset };
90
+
91
+ // Auto-generate secrets if not already set — never prompt for these
92
+ if (!String(base.JWT_SECRET ?? '').trim()) {
93
+ base.JWT_SECRET = randomBytes(32).toString('hex');
94
+ }
95
+
96
+ const current = normalizeEnv(base);
107
97
 
108
98
  if (yes) {
109
99
  await writeEnvFile(envFile, current);
@@ -111,21 +101,13 @@ export async function promptForEnv({ envFile, existing, yes = false, preset = {}
111
101
  }
112
102
 
113
103
  const questions = [
114
- { name: 'DISCORD_CLIENT_ID', message: 'Discord Client ID' },
115
- { name: 'DISCORD_CLIENT_SECRET', message: 'Discord Client Secret', secret: true },
116
- { name: 'DISCORD_GUILD_ID', message: 'Discord Guild ID' },
117
- { name: 'OWNER_DISCORD_ID', message: 'Managed vault owner Discord ID' },
118
- { name: 'JWT_SECRET', message: 'JWT secret', secret: true },
119
- { name: 'VAULT_PATH', message: 'Vault absolute path' },
120
104
  { name: 'PORT', message: 'HTTP port' },
121
- { name: 'YJS_PORT', message: 'Yjs WS port' },
122
- { name: 'DISCORD_REDIRECT_URI', message: 'Discord redirect URI' },
123
105
  ];
124
106
 
125
107
  const answers = {};
126
108
  for (const q of questions) {
127
109
  const response = await prompts({
128
- type: q.secret ? 'password' : 'text',
110
+ type: 'text',
129
111
  name: 'value',
130
112
  message: q.message,
131
113
  initial: current[q.name] ?? '',
@@ -64,16 +64,11 @@ export async function runDoctorChecks({ envFile, includeCloudflared = true }) {
64
64
  }
65
65
  }
66
66
 
67
- if (env.VAULT_PATH && env.OWNER_DISCORD_ID) {
67
+ if (env.VAULT_PATH) {
68
68
  try {
69
69
  const managedState = await loadManagedState(env.VAULT_PATH);
70
70
  if (!managedState) {
71
71
  info('Managed state not initialized yet');
72
- } else if (managedState.ownerDiscordId !== env.OWNER_DISCORD_ID) {
73
- fail(
74
- `Managed owner mismatch: state=${managedState.ownerDiscordId} env=${env.OWNER_DISCORD_ID}`,
75
- );
76
- failures += 1;
77
72
  } else {
78
73
  success(`Managed state OK (vaultId ${managedState.vaultId})`);
79
74
  }
@@ -5,11 +5,11 @@ import {
5
5
  DEFAULT_CLOUDFLARED_CONFIG,
6
6
  DEFAULT_TUNNEL_NAME,
7
7
  EXIT,
8
+ HIVE_HOME,
8
9
  HIVE_CONFIG_FILE,
9
- LEGACY_ENV_FILE,
10
10
  } from '../constants.js';
11
11
  import { CliError } from '../errors.js';
12
- import { inferDomainFromRedirect, loadEnvFile, normalizeEnv, promptForEnv, validateEnvValues, writeEnvFile } from '../env-file.js';
12
+ import { loadEnvFile, normalizeEnv, promptForEnv, validateEnvValues, writeEnvFile } from '../env-file.js';
13
13
  import { validateDomain } from '../checks.js';
14
14
  import { updateHiveConfig } from '../config.js';
15
15
  import { installHiveService } from '../service.js';
@@ -20,9 +20,11 @@ import {
20
20
  promptConfirm,
21
21
  requiredOrFallback,
22
22
  resolveContext,
23
- setRedirectUriForDomain,
24
23
  } from '../core/context.js';
25
24
  import { runDoctorChecks } from './doctor.js';
25
+ import { createVaultAtParent, initializeOwnerManagedVault } from '../../lib/setupOrchestrator.js';
26
+ import { loadManagedState } from '../../lib/managedState.js';
27
+ import { join } from 'path';
26
28
 
27
29
  export async function runSetupWizard(options) {
28
30
  const yes = Boolean(options.yes);
@@ -31,25 +33,10 @@ export async function runSetupWizard(options) {
31
33
 
32
34
  const { config, envFile } = await resolveContext(options);
33
35
  let nextConfig = { ...config, envFile };
34
- let importedLegacy = false;
35
-
36
- if (!existsSync(HIVE_CONFIG_FILE) && existsSync(LEGACY_ENV_FILE)) {
37
- const shouldImportLegacy = await promptConfirm(
38
- `Import existing legacy env from ${LEGACY_ENV_FILE}?`,
39
- yes,
40
- true
41
- );
42
- if (shouldImportLegacy) {
43
- const legacyValues = await loadEnvFile(LEGACY_ENV_FILE);
44
- await writeEnvFile(envFile, legacyValues);
45
- importedLegacy = true;
46
- success(`Imported legacy env into ${envFile}`);
47
- }
48
- }
49
36
 
50
37
  const envExists = existsSync(envFile);
51
38
  let envValues;
52
- if (!envExists || importedLegacy) {
39
+ if (!envExists) {
53
40
  info(`Initializing env file at ${envFile}`);
54
41
  const existing = await loadEnvFile(envFile);
55
42
  envValues = await promptForEnv({ envFile, existing, yes });
@@ -63,39 +50,121 @@ export async function runSetupWizard(options) {
63
50
  }
64
51
  }
65
52
 
53
+ // Prompt for vault display name and owner account credentials
54
+ let vaultName = '';
55
+ let vaultParentPath = '';
56
+ let ownerEmail = '';
57
+ let ownerDisplayName = '';
58
+ let ownerPassword = '';
59
+ if (!yes) {
60
+ const nameResp = await prompts({
61
+ type: 'text',
62
+ name: 'name',
63
+ message: 'Vault display name (e.g. "Team Vault")',
64
+ validate: (v) => String(v).trim().length > 0 || 'Vault name is required',
65
+ });
66
+ vaultName = String(nameResp.name ?? '').trim();
67
+
68
+ if (!String(envValues.VAULT_PATH ?? '').trim()) {
69
+ const parentResp = await prompts({
70
+ type: 'text',
71
+ name: 'path',
72
+ message: 'Choose folder location for the generated vault (parent directory)',
73
+ validate: (v) => String(v).trim().length > 0 || 'Parent folder path is required',
74
+ });
75
+ vaultParentPath = String(parentResp.path ?? '').trim();
76
+ }
77
+
78
+ const acctResp = await prompts([
79
+ { type: 'text', name: 'email', message: 'Owner email (for dashboard login)' },
80
+ { type: 'text', name: 'displayName', message: 'Owner display name' },
81
+ { type: 'password', name: 'password', message: 'Owner password' },
82
+ ]);
83
+ ownerEmail = String(acctResp.email ?? '').trim();
84
+ ownerDisplayName = String(acctResp.displayName ?? '').trim() || 'Owner';
85
+ ownerPassword = String(acctResp.password ?? '');
86
+ }
87
+
88
+ const existingVaultPath = String(envValues.VAULT_PATH ?? '').trim();
89
+ if (!existingVaultPath) {
90
+ if (yes) {
91
+ vaultName = vaultName || 'Hive Vault';
92
+ vaultParentPath = vaultParentPath || join(HIVE_HOME, 'vaults');
93
+ }
94
+ if (vaultName && vaultParentPath) {
95
+ const generatedVaultPath = await createVaultAtParent({
96
+ parentPath: vaultParentPath,
97
+ vaultName,
98
+ });
99
+ envValues = { ...envValues, VAULT_PATH: generatedVaultPath };
100
+ await writeEnvFile(envFile, envValues);
101
+ success(`Generated vault at ${generatedVaultPath}`);
102
+ }
103
+ }
104
+
66
105
  const envIssues = validateEnvValues(envValues);
67
106
  if (envIssues.length > 0) {
68
107
  for (const issue of envIssues) fail(issue);
69
108
  throw new CliError('Env configuration is invalid', EXIT.FAIL);
70
109
  }
71
110
 
72
- let domain = requiredOrFallback(
73
- options.domain,
74
- inferDomainFromRedirect(envValues.DISCORD_REDIRECT_URI) || nextConfig.domain
111
+ // Initialize owner account + managed vault when interactive setup provides credentials.
112
+ if (vaultName && ownerEmail && ownerPassword) {
113
+ const existingState = await loadManagedState(String(envValues.VAULT_PATH));
114
+ if (existingState) {
115
+ info('Managed vault already initialized; skipping owner and vault initialization.');
116
+ } else {
117
+ await initializeOwnerManagedVault({
118
+ vaultPath: envValues.VAULT_PATH,
119
+ vaultName,
120
+ ownerEmail,
121
+ ownerDisplayName,
122
+ ownerPassword,
123
+ });
124
+ success('Owner account created');
125
+ success(`Vault initialized: ${vaultName}`);
126
+ }
127
+ }
128
+
129
+ // Prompt for public server URL (used for invite claim redirects)
130
+ let hiveServerUrl = requiredOrFallback(
131
+ options.domain ? `https://${options.domain}` : '',
132
+ envValues.HIVE_SERVER_URL || (nextConfig.domain ? `https://${nextConfig.domain}` : ''),
75
133
  );
76
134
 
77
135
  if (!yes) {
78
136
  const response = await prompts({
79
137
  type: 'text',
80
- name: 'domain',
81
- message: 'Public domain for Hive server',
82
- initial: domain,
138
+ name: 'url',
139
+ message: 'Public URL for Hive server (optional, used for invite links)',
140
+ initial: hiveServerUrl || '',
83
141
  });
84
- if (response.domain !== undefined) {
85
- domain = String(response.domain).trim();
142
+ if (response.url !== undefined) {
143
+ hiveServerUrl = String(response.url).trim();
86
144
  }
87
145
  }
88
146
 
89
- if (!validateDomain(domain)) {
90
- throw new CliError(`Invalid domain: ${domain}`);
91
- }
147
+ if (hiveServerUrl) {
148
+ const updated = { ...envValues, HIVE_SERVER_URL: hiveServerUrl };
149
+ await writeEnvFile(envFile, updated);
150
+ envValues = updated;
151
+
152
+ // Derive domain for Cloudflare Tunnel config
153
+ let domain = nextConfig.domain;
154
+ try {
155
+ domain = new URL(hiveServerUrl).hostname;
156
+ } catch {
157
+ // ignore parse error
158
+ }
92
159
 
93
- envValues = await setRedirectUriForDomain({ envFile, env: envValues, domain, yes });
160
+ if (domain && validateDomain(domain)) {
161
+ nextConfig = { ...nextConfig, domain };
162
+ }
163
+ }
94
164
 
95
165
  const shouldSetupTunnel = await promptConfirm('Configure Cloudflare Tunnel now?', yes, true);
96
166
  if (shouldSetupTunnel) {
97
167
  const port = parseInteger(envValues.PORT, 'PORT');
98
- const yjsPort = parseInteger(envValues.YJS_PORT, 'YJS_PORT');
99
168
  const tunnelName = requiredOrFallback(options.tunnelName, nextConfig.tunnelName || DEFAULT_TUNNEL_NAME);
100
169
  const cloudflaredConfigFile = requiredOrFallback(
101
170
  options.cloudflaredConfigFile,
@@ -103,13 +172,17 @@ export async function runSetupWizard(options) {
103
172
  );
104
173
  const tunnelService = await promptConfirm('Install cloudflared as a service?', yes, true);
105
174
 
175
+ const domain = nextConfig.domain;
176
+ if (!domain || !validateDomain(domain)) {
177
+ throw new CliError(`Cannot configure tunnel: invalid or missing domain (${domain})`);
178
+ }
179
+
106
180
  const tunnelResult = await setupTunnel({
107
181
  tunnelName,
108
182
  domain,
109
183
  configFile: cloudflaredConfigFile,
110
184
  certPath: DEFAULT_CLOUDFLARED_CERT,
111
185
  port,
112
- yjsPort,
113
186
  yes,
114
187
  installService: tunnelService,
115
188
  });
package/cli/tunnel.js CHANGED
@@ -138,10 +138,9 @@ export async function writeCloudflaredConfig({
138
138
  credentialsFile,
139
139
  domain,
140
140
  port,
141
- yjsPort,
142
141
  }) {
143
142
  await mkdir(dirname(configFile), { recursive: true });
144
- const yaml = `tunnel: ${tunnelId}\ncredentials-file: ${credentialsFile}\n\ningress:\n - hostname: ${domain}\n path: /yjs/*\n service: http://localhost:${yjsPort}\n\n - hostname: ${domain}\n service: http://localhost:${port}\n\n - service: http_status:404\n`;
143
+ const yaml = `tunnel: ${tunnelId}\ncredentials-file: ${credentialsFile}\n\ningress:\n - hostname: ${domain}\n service: http://localhost:${port}\n\n - service: http_status:404\n`;
145
144
  await writeFile(configFile, yaml, 'utf-8');
146
145
  return yaml;
147
146
  }
@@ -358,7 +357,6 @@ export async function setupTunnel({
358
357
  configFile,
359
358
  certPath,
360
359
  port,
361
- yjsPort,
362
360
  yes = false,
363
361
  installService = false,
364
362
  }) {
@@ -379,7 +377,6 @@ export async function setupTunnel({
379
377
  credentialsFile,
380
378
  domain,
381
379
  port,
382
- yjsPort,
383
380
  });
384
381
 
385
382
  info(`Ensuring DNS route for ${domain}`);
package/index.js CHANGED
@@ -1,26 +1,20 @@
1
1
  import dotenv from 'dotenv';
2
2
  import { createServer } from 'http';
3
+ import { existsSync } from 'fs';
3
4
  import { fileURLToPath } from 'url';
4
5
  import express from 'express';
5
6
  import { Server } from 'socket.io';
6
7
  import authRoutes from './routes/auth.js';
7
- import managedRoutes from './routes/managed.js';
8
+ import dashboardRoutes from './routes/dashboard.js';
8
9
  import * as vault from './lib/vaultManager.js';
9
10
  import { attachHandlers } from './lib/socketHandler.js';
10
11
  import { startYjsServer, getActiveRooms, forceCloseRoom, getRoomStatus } from './lib/yjsServer.js';
11
- import { assertOwnerConsistency } from './lib/managedState.js';
12
-
13
- const REQUIRED = [
14
- 'VAULT_PATH',
15
- 'JWT_SECRET',
16
- 'DISCORD_CLIENT_ID',
17
- 'DISCORD_CLIENT_SECRET',
18
- 'DISCORD_REDIRECT_URI',
19
- 'DISCORD_GUILD_ID',
20
- 'OWNER_DISCORD_ID',
21
- ];
22
-
23
- function validateEnv() {
12
+ import { loadManagedState } from './lib/managedState.js';
13
+ import { DEFAULT_ENV_FILE } from './cli/constants.js';
14
+
15
+ const REQUIRED = ['JWT_SECRET'];
16
+
17
+ function validateEnv({ allowSetupMode = false } = {}) {
24
18
  for (const key of REQUIRED) {
25
19
  if (!process.env[key]) {
26
20
  throw new Error(`[startup] Missing required env var: ${key}`);
@@ -28,24 +22,31 @@ function validateEnv() {
28
22
  }
29
23
 
30
24
  const port = parseInt(process.env.PORT ?? '3000', 10);
31
- const yjsPort = parseInt(process.env.YJS_PORT ?? '3001', 10);
32
25
  if (!Number.isInteger(port) || port <= 0) {
33
26
  throw new Error('[startup] PORT must be a positive integer');
34
27
  }
35
- if (!Number.isInteger(yjsPort) || yjsPort <= 0) {
36
- throw new Error('[startup] YJS_PORT must be a positive integer');
28
+
29
+ // Ensure packaged onboarding assets are available on startup.
30
+ const assetsRoot = fileURLToPath(new URL('./assets', import.meta.url));
31
+ if (!existsSync(assetsRoot)) {
32
+ throw new Error('[startup] Missing packaged assets directory');
37
33
  }
38
34
 
39
- return { port };
35
+ const hasVaultPath = Boolean(String(process.env.VAULT_PATH ?? '').trim());
36
+ if (!hasVaultPath && !allowSetupMode) {
37
+ throw new Error('[startup] Missing required env var: VAULT_PATH');
38
+ }
39
+
40
+ return { port, hasVaultPath };
40
41
  }
41
42
 
42
43
  /**
43
44
  * Start Hive server runtime.
44
45
  *
45
- * @param {{ envFile?: string, quiet?: boolean }} [options]
46
+ * @param {{ envFile?: string, quiet?: boolean, allowSetupMode?: boolean }} [options]
46
47
  */
47
48
  export async function startHiveServer(options = {}) {
48
- const { envFile, quiet = false } = options;
49
+ const { envFile, quiet = false, allowSetupMode = false } = options;
49
50
 
50
51
  dotenv.config(
51
52
  envFile
@@ -53,11 +54,17 @@ export async function startHiveServer(options = {}) {
53
54
  : undefined
54
55
  );
55
56
 
56
- const { port } = validateEnv();
57
- await assertOwnerConsistency(process.env.VAULT_PATH, process.env.OWNER_DISCORD_ID);
57
+ process.env.HIVE_ENV_FILE = envFile || process.env.HIVE_ENV_FILE || DEFAULT_ENV_FILE;
58
+
59
+ const { port, hasVaultPath } = validateEnv({ allowSetupMode });
60
+ if (hasVaultPath) {
61
+ await loadManagedState(process.env.VAULT_PATH);
62
+ }
58
63
 
59
64
  const app = express();
60
65
  const httpServer = createServer(app);
66
+ app.locals.hiveEnvFile = process.env.HIVE_ENV_FILE;
67
+ app.locals.activateRealtime = null;
61
68
 
62
69
  const io = new Server(httpServer, {
63
70
  cors: {
@@ -68,9 +75,28 @@ export async function startHiveServer(options = {}) {
68
75
 
69
76
  app.use(express.json());
70
77
 
78
+ // Allow desktop plugin fetch calls (Authorization header triggers preflight).
79
+ app.use((req, res, next) => {
80
+ res.setHeader('Access-Control-Allow-Origin', '*');
81
+ res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
82
+ res.setHeader('Access-Control-Allow-Headers', 'Authorization,Content-Type');
83
+ if (req.method === 'OPTIONS') {
84
+ res.sendStatus(204);
85
+ return;
86
+ }
87
+ next();
88
+ });
89
+
90
+ const setupRequired = (req, res, next) => {
91
+ if (!String(process.env.VAULT_PATH ?? '').trim()) {
92
+ return res.status(503).json({ ok: false, error: 'Setup required: configure vault path first.' });
93
+ }
94
+ next();
95
+ };
96
+
71
97
  // Auth routes
72
- app.use('/auth', authRoutes);
73
- app.use('/managed', managedRoutes);
98
+ app.use('/auth', setupRequired, authRoutes);
99
+ app.use('/dashboard', dashboardRoutes);
74
100
 
75
101
  // Health check
76
102
  app.get('/health', (req, res) => res.json({ ok: true, ts: Date.now() }));
@@ -84,23 +110,46 @@ export async function startHiveServer(options = {}) {
84
110
  });
85
111
  }
86
112
 
87
- attachHandlers(io, getActiveRooms, broadcastFileUpdated, forceCloseRoom);
88
- startYjsServer(broadcastFileUpdated);
113
+ let realtimeActive = false;
114
+ let yjsWss = null;
115
+ const activateRealtime = async () => {
116
+ if (realtimeActive) return;
117
+ const vaultPath = String(process.env.VAULT_PATH ?? '').trim();
118
+ if (!vaultPath) return;
119
+ await loadManagedState(vaultPath);
120
+ attachHandlers(io, getActiveRooms, broadcastFileUpdated, forceCloseRoom);
121
+ yjsWss = startYjsServer(httpServer, broadcastFileUpdated);
122
+ httpServer.on('upgrade', (req, socket, head) => {
123
+ const { pathname } = new URL(req.url, 'http://localhost');
124
+ if (pathname.startsWith('/yjs')) {
125
+ yjsWss.handleUpgrade(req, socket, head, (ws) => {
126
+ yjsWss.emit('connection', ws, req);
127
+ });
128
+ }
129
+ // Socket.IO handles /socket.io upgrades automatically via its own listener
130
+ });
89
131
 
90
- // Chokidar watch for external (non-plugin) changes
91
- vault.initWatcher((relPath, event) => {
92
- const docName = encodeURIComponent(relPath);
93
- if (getActiveRooms().has(docName)) {
132
+ vault.initWatcher((relPath, event) => {
133
+ const docName = encodeURIComponent(relPath);
134
+ if (getActiveRooms().has(docName)) {
135
+ if (!quiet) {
136
+ console.log(`[chokidar] Ignoring external change to active room: ${relPath}`);
137
+ }
138
+ return;
139
+ }
94
140
  if (!quiet) {
95
- console.log(`[chokidar] Ignoring external change to active room: ${relPath}`);
141
+ console.log(`[chokidar] External ${event}: ${relPath}`);
96
142
  }
97
- return;
98
- }
99
- if (!quiet) {
100
- console.log(`[chokidar] External ${event}: ${relPath}`);
101
- }
102
- io.emit('external-update', { relPath, event });
103
- });
143
+ io.emit('external-update', { relPath, event });
144
+ });
145
+
146
+ realtimeActive = true;
147
+ };
148
+ app.locals.activateRealtime = activateRealtime;
149
+
150
+ if (hasVaultPath) {
151
+ await activateRealtime();
152
+ }
104
153
 
105
154
  await new Promise((resolve, reject) => {
106
155
  httpServer.once('error', reject);
@@ -109,7 +158,11 @@ export async function startHiveServer(options = {}) {
109
158
 
110
159
  if (!quiet) {
111
160
  console.log(`[server] Hive server listening on port ${port}`);
112
- console.log(`[server] Vault: ${process.env.VAULT_PATH}`);
161
+ if (String(process.env.VAULT_PATH ?? '').trim()) {
162
+ console.log(`[server] Vault: ${process.env.VAULT_PATH}`);
163
+ } else {
164
+ console.log('[server] Setup mode: VAULT_PATH not configured yet');
165
+ }
113
166
  }
114
167
 
115
168
  return {