@fyresmith/hive-server 4.0.1 → 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.
@@ -0,0 +1,33 @@
1
+ {
2
+ "file-explorer": true,
3
+ "global-search": true,
4
+ "switcher": true,
5
+ "graph": true,
6
+ "backlink": true,
7
+ "canvas": true,
8
+ "outgoing-link": true,
9
+ "tag-pane": true,
10
+ "footnotes": false,
11
+ "properties": true,
12
+ "page-preview": true,
13
+ "daily-notes": true,
14
+ "templates": true,
15
+ "note-composer": true,
16
+ "command-palette": true,
17
+ "slash-command": false,
18
+ "editor-status": true,
19
+ "bookmarks": true,
20
+ "markdown-importer": false,
21
+ "zk-prefixer": false,
22
+ "random-note": false,
23
+ "outline": true,
24
+ "word-count": true,
25
+ "slides": false,
26
+ "audio-recorder": false,
27
+ "workspaces": false,
28
+ "file-recovery": true,
29
+ "publish": false,
30
+ "sync": true,
31
+ "bases": true,
32
+ "webviewer": false
33
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "collapse-filter": true,
3
+ "search": "",
4
+ "showTags": false,
5
+ "showAttachments": false,
6
+ "hideUnresolved": false,
7
+ "showOrphans": true,
8
+ "collapse-color-groups": true,
9
+ "colorGroups": [],
10
+ "collapse-display": true,
11
+ "showArrow": false,
12
+ "textFadeMultiplier": 0,
13
+ "nodeSizeMultiplier": 1,
14
+ "lineSizeMultiplier": 1,
15
+ "collapse-forces": true,
16
+ "centerStrength": 0.518713248970312,
17
+ "repelStrength": 10,
18
+ "linkStrength": 1,
19
+ "linkDistance": 250,
20
+ "scale": 1,
21
+ "close": true
22
+ }
@@ -0,0 +1,206 @@
1
+ {
2
+ "main": {
3
+ "id": "7b9f3c22c91d897c",
4
+ "type": "split",
5
+ "children": [
6
+ {
7
+ "id": "eea5e6133fb6313d",
8
+ "type": "tabs",
9
+ "children": [
10
+ {
11
+ "id": "20957127bee176ea",
12
+ "type": "leaf",
13
+ "state": {
14
+ "type": "markdown",
15
+ "state": {
16
+ "file": "Welcome.md",
17
+ "mode": "source",
18
+ "source": false
19
+ },
20
+ "icon": "lucide-file",
21
+ "title": "Welcome"
22
+ }
23
+ }
24
+ ]
25
+ },
26
+ {
27
+ "id": "3606ffb170dd4603",
28
+ "type": "tabs",
29
+ "children": [
30
+ {
31
+ "id": "457fdb78bef02573",
32
+ "type": "leaf",
33
+ "state": {
34
+ "type": "graph",
35
+ "state": {},
36
+ "icon": "lucide-git-fork",
37
+ "title": "Graph view"
38
+ }
39
+ }
40
+ ]
41
+ }
42
+ ],
43
+ "direction": "vertical"
44
+ },
45
+ "left": {
46
+ "id": "c93fdc6c295009fd",
47
+ "type": "split",
48
+ "children": [
49
+ {
50
+ "id": "0b2bc1cb1a6c5fdb",
51
+ "type": "tabs",
52
+ "children": [
53
+ {
54
+ "id": "332749683c18294c",
55
+ "type": "leaf",
56
+ "state": {
57
+ "type": "file-explorer",
58
+ "state": {
59
+ "sortOrder": "alphabetical",
60
+ "autoReveal": false
61
+ },
62
+ "icon": "lucide-folder-closed",
63
+ "title": "Files"
64
+ }
65
+ },
66
+ {
67
+ "id": "2824dbf53a5c4597",
68
+ "type": "leaf",
69
+ "state": {
70
+ "type": "search",
71
+ "state": {
72
+ "query": "",
73
+ "matchingCase": false,
74
+ "explainSearch": false,
75
+ "collapseAll": false,
76
+ "extraContext": false,
77
+ "sortOrder": "alphabetical"
78
+ },
79
+ "icon": "lucide-search",
80
+ "title": "Search"
81
+ }
82
+ },
83
+ {
84
+ "id": "9f275c920d60b878",
85
+ "type": "leaf",
86
+ "state": {
87
+ "type": "bookmarks",
88
+ "state": {},
89
+ "icon": "lucide-bookmark",
90
+ "title": "Bookmarks"
91
+ }
92
+ }
93
+ ]
94
+ }
95
+ ],
96
+ "direction": "horizontal",
97
+ "width": 300
98
+ },
99
+ "right": {
100
+ "id": "720628f43492de54",
101
+ "type": "split",
102
+ "children": [
103
+ {
104
+ "id": "e2e6ee489b84f739",
105
+ "type": "tabs",
106
+ "children": [
107
+ {
108
+ "id": "97a8f2230c937318",
109
+ "type": "leaf",
110
+ "state": {
111
+ "type": "backlink",
112
+ "state": {
113
+ "file": "Welcome.md",
114
+ "collapseAll": false,
115
+ "extraContext": false,
116
+ "sortOrder": "alphabetical",
117
+ "showSearch": false,
118
+ "searchQuery": "",
119
+ "backlinkCollapsed": false,
120
+ "unlinkedCollapsed": true
121
+ },
122
+ "icon": "links-coming-in",
123
+ "title": "Backlinks for Welcome"
124
+ }
125
+ },
126
+ {
127
+ "id": "2a344f218762d53a",
128
+ "type": "leaf",
129
+ "state": {
130
+ "type": "outgoing-link",
131
+ "state": {
132
+ "file": "Welcome.md",
133
+ "linksCollapsed": false,
134
+ "unlinkedCollapsed": true
135
+ },
136
+ "icon": "links-going-out",
137
+ "title": "Outgoing links from Welcome"
138
+ }
139
+ },
140
+ {
141
+ "id": "072e764fb5cc77fa",
142
+ "type": "leaf",
143
+ "state": {
144
+ "type": "tag",
145
+ "state": {
146
+ "sortOrder": "frequency",
147
+ "useHierarchy": true,
148
+ "showSearch": false,
149
+ "searchQuery": ""
150
+ },
151
+ "icon": "lucide-tags",
152
+ "title": "Tags"
153
+ }
154
+ },
155
+ {
156
+ "id": "f304846ef7927a68",
157
+ "type": "leaf",
158
+ "state": {
159
+ "type": "all-properties",
160
+ "state": {
161
+ "sortOrder": "frequency",
162
+ "showSearch": false,
163
+ "searchQuery": ""
164
+ },
165
+ "icon": "lucide-archive",
166
+ "title": "All properties"
167
+ }
168
+ },
169
+ {
170
+ "id": "de791fb3b1b7afdb",
171
+ "type": "leaf",
172
+ "state": {
173
+ "type": "outline",
174
+ "state": {
175
+ "file": "Welcome.md",
176
+ "followCursor": false,
177
+ "showSearch": false,
178
+ "searchQuery": ""
179
+ },
180
+ "icon": "lucide-list",
181
+ "title": "Outline of Welcome"
182
+ }
183
+ }
184
+ ]
185
+ }
186
+ ],
187
+ "direction": "horizontal",
188
+ "width": 300,
189
+ "collapsed": true
190
+ },
191
+ "left-ribbon": {
192
+ "hiddenItems": {
193
+ "switcher:Open quick switcher": false,
194
+ "graph:Open graph view": false,
195
+ "canvas:Create new canvas": false,
196
+ "daily-notes:Open today's daily note": false,
197
+ "templates:Insert template": false,
198
+ "command-palette:Open command palette": false,
199
+ "bases:Create new base": false
200
+ }
201
+ },
202
+ "active": "20957127bee176ea",
203
+ "lastOpenFiles": [
204
+ "Welcome.md"
205
+ ]
206
+ }
@@ -0,0 +1,5 @@
1
+ This is your new *vault*.
2
+
3
+ Make a note of something, [[create a link]], or try [the Importer](https://help.obsidian.md/Plugins/Importer)!
4
+
5
+ When you're ready, delete this note and make the vault your own.
@@ -1,7 +1,7 @@
1
1
  import { section, success, fail } from '../output.js';
2
2
  import { EXIT } from '../constants.js';
3
3
  import { CliError } from '../errors.js';
4
- import { inferDomainFromRedirect, loadEnvFile, normalizeEnv, promptForEnv, redactEnv, validateEnvValues } from '../env-file.js';
4
+ import { loadEnvFile, normalizeEnv, promptForEnv, redactEnv, validateEnvValues } from '../env-file.js';
5
5
  import { updateHiveConfig } from '../config.js';
6
6
  import { assertEnvFileExists, loadValidatedEnv, resolveContext } from '../core/context.js';
7
7
 
@@ -18,13 +18,13 @@ export function registerEnvCommands(program) {
18
18
  const { config, envFile } = await resolveContext(options);
19
19
  const existing = await loadEnvFile(envFile);
20
20
  const values = await promptForEnv({ envFile, existing, yes: options.yes });
21
- const issues = validateEnvValues(values);
21
+ const issues = validateEnvValues(values, { requireVaultPath: false });
22
22
  if (issues.length > 0) {
23
23
  for (const issue of issues) fail(issue);
24
24
  throw new CliError('Env file has validation issues', EXIT.FAIL);
25
25
  }
26
26
 
27
- const domain = inferDomainFromRedirect(values.DISCORD_REDIRECT_URI) || config.domain;
27
+ const domain = config.domain;
28
28
  await updateHiveConfig({ envFile, domain });
29
29
  success(`Env file ready at ${envFile}`);
30
30
  });
@@ -41,7 +41,7 @@ export function registerEnvCommands(program) {
41
41
 
42
42
  const existing = await loadEnvFile(envFile);
43
43
  const values = await promptForEnv({ envFile, existing, yes: options.yes });
44
- const issues = validateEnvValues(values);
44
+ const issues = validateEnvValues(values, { requireVaultPath: false });
45
45
  if (issues.length > 0) {
46
46
  for (const issue of issues) fail(issue);
47
47
  throw new CliError('Env file has validation issues', EXIT.FAIL);
@@ -19,14 +19,14 @@ async function resolveManagedInputs(options) {
19
19
  }
20
20
  return {
21
21
  vaultPath: env.VAULT_PATH,
22
- ownerDiscordId: env.OWNER_DISCORD_ID,
22
+ hiveServerUrl: env.HIVE_SERVER_URL || '',
23
23
  envFile,
24
24
  };
25
25
  }
26
26
 
27
27
  function assertInitialized(state) {
28
28
  if (!state) {
29
- throw new CliError('Managed vault is not initialized. Run pair/init flow first.', EXIT.FAIL);
29
+ throw new CliError('Managed vault is not initialized. Run hive setup or dashboard setup first.', EXIT.FAIL);
30
30
  }
31
31
  }
32
32
 
@@ -38,21 +38,19 @@ export function registerManagedCommands(program) {
38
38
  .description('Show managed vault status')
39
39
  .option('--env-file <path>', 'env file path')
40
40
  .action(async (options) => {
41
- const { vaultPath, ownerDiscordId, envFile } = await resolveManagedInputs(options);
41
+ const { vaultPath, envFile } = await resolveManagedInputs(options);
42
42
  const state = await loadManagedState(vaultPath);
43
43
  section('Managed Status');
44
44
  console.log(`Env: ${envFile}`);
45
45
  if (!state) {
46
46
  console.log('Initialized: no');
47
- console.log(`Configured owner: ${ownerDiscordId}`);
48
47
  return;
49
48
  }
50
- const status = describeManagedStatus(state, ownerDiscordId);
49
+ const status = describeManagedStatus(state, state.ownerId);
51
50
  console.log('Initialized: yes');
51
+ console.log(`Vault Name: ${state.vaultName ?? '(not set)'}`);
52
52
  console.log(`Vault ID: ${status.vaultId}`);
53
- console.log(`Owner: ${state.ownerDiscordId}`);
54
- console.log(`Configured owner: ${ownerDiscordId}`);
55
- console.log(`Owner matches env: ${state.ownerDiscordId === ownerDiscordId ? 'yes' : 'no'}`);
53
+ console.log(`Owner: ${state.ownerId}`);
56
54
  console.log(`Members: ${status.memberCount}`);
57
55
  console.log(`Invites: ${Object.keys(state.invites ?? {}).length}`);
58
56
  });
@@ -64,13 +62,20 @@ export function registerManagedCommands(program) {
64
62
  .description('Create a single-use invite code')
65
63
  .option('--env-file <path>', 'env file path')
66
64
  .action(async (options) => {
67
- const { vaultPath, ownerDiscordId } = await resolveManagedInputs(options);
65
+ const { vaultPath, hiveServerUrl } = await resolveManagedInputs(options);
66
+ const state = await loadManagedState(vaultPath);
67
+ if (!state) {
68
+ throw new CliError('Managed vault is not initialized. Run hive setup or dashboard setup.', EXIT.FAIL);
69
+ }
68
70
  const created = await createInvite({
69
71
  vaultPath,
70
- ownerDiscordId,
71
- createdBy: ownerDiscordId,
72
+ createdBy: state.ownerId,
72
73
  });
73
74
  success(`Invite created: ${created.code}`);
75
+ if (hiveServerUrl) {
76
+ console.log(`Claim URL: ${hiveServerUrl}/auth/claim?code=${created.code}`);
77
+ console.log('Next: recipient opens claim URL, signs in, then downloads the Hive vault shell.');
78
+ }
74
79
  });
75
80
 
76
81
  invite
@@ -122,21 +127,21 @@ export function registerManagedCommands(program) {
122
127
  return;
123
128
  }
124
129
  for (const row of members) {
125
- const ownerMark = row.id === state.ownerDiscordId ? ' (owner)' : '';
130
+ const ownerMark = row.id === state.ownerId ? ' (owner)' : '';
126
131
  console.log(`${row.id}${ownerMark} @${row.username} added ${row.addedAt}`);
127
132
  }
128
133
  });
129
134
 
130
135
  member
131
- .command('remove <discordId>')
136
+ .command('remove <userId>')
132
137
  .description('Remove a paired member')
133
138
  .option('--env-file <path>', 'env file path')
134
- .action(async (discordId, options) => {
139
+ .action(async (userId, options) => {
135
140
  const { vaultPath } = await resolveManagedInputs(options);
136
- const result = await removeMember({ vaultPath, discordId });
141
+ const result = await removeMember({ vaultPath, userId });
137
142
  if (!result.removed) {
138
- throw new CliError(`Member not found: ${discordId}`, EXIT.FAIL);
143
+ throw new CliError(`Member not found: ${userId}`, EXIT.FAIL);
139
144
  }
140
- success(`Removed member: ${discordId}`);
145
+ success(`Removed member: ${userId}`);
141
146
  });
142
147
  }
@@ -1,15 +1,19 @@
1
1
  import { existsSync } from 'fs';
2
+ import { platform } from 'os';
2
3
  import { HIVE_CONFIG_FILE, EXIT } from '../constants.js';
3
4
  import { CliError } from '../errors.js';
4
5
  import { loadHiveConfig } from '../config.js';
5
- import { section, info } from '../output.js';
6
+ import { section, info, success } from '../output.js';
6
7
  import { cloudflaredServiceStatus } from '../tunnel.js';
7
8
  import { getHiveServiceStatus } from '../service.js';
8
- import { resolveContext, resolveServiceConfig } from '../core/context.js';
9
+ import { resolveContext, resolveServiceConfig, loadValidatedEnv } from '../core/context.js';
10
+ import { loadEnvFile, normalizeEnv, writeEnvFile } from '../env-file.js';
9
11
  import { runDoctorChecks } from '../flows/doctor.js';
10
12
  import { runSetupWizard } from '../flows/setup.js';
11
13
  import { runDownFlow, runLogsFlow, runUpFlow, runUpdateFlow } from '../flows/system.js';
12
14
  import { startHiveServer } from '../../index.js';
15
+ import { run } from '../exec.js';
16
+ import { randomBytes } from 'crypto';
13
17
 
14
18
  export function registerRootCommands(program) {
15
19
  program
@@ -69,6 +73,48 @@ export function registerRootCommands(program) {
69
73
  info(`Hive server started using env: ${envFile}`);
70
74
  });
71
75
 
76
+ program
77
+ .command('dashboard')
78
+ .description('Start/open the owner dashboard')
79
+ .option('--env-file <path>', 'env file path')
80
+ .action(async (options) => {
81
+ const { envFile } = await resolveContext(options);
82
+ const { env } = await loadValidatedEnv(envFile, { requireFile: false, requireVaultPath: false });
83
+
84
+ const port = String(env.PORT || '3000').trim();
85
+ const serverUrl = String(env.HIVE_SERVER_URL || '').trim() || `http://localhost:${port}`;
86
+ const dashboardUrl = `${serverUrl}/dashboard`;
87
+ const localUrl = `http://127.0.0.1:${port}`;
88
+ const useLocalRuntime = !String(env.HIVE_SERVER_URL || '').trim();
89
+
90
+ if (useLocalRuntime) {
91
+ if (!String(env.JWT_SECRET || '').trim()) {
92
+ const existing = await loadEnvFile(envFile);
93
+ const next = normalizeEnv(existing);
94
+ next.JWT_SECRET = randomBytes(32).toString('hex');
95
+ await writeEnvFile(envFile, next);
96
+ env.JWT_SECRET = next.JWT_SECRET;
97
+ info(`Generated JWT_SECRET in ${envFile}`);
98
+ }
99
+
100
+ const health = await fetch(`${localUrl}/health`).then((res) => res.ok).catch(() => false);
101
+ if (!health) {
102
+ await startHiveServer({ envFile, quiet: true, allowSetupMode: true });
103
+ info(`Hive server started using env: ${envFile}`);
104
+ }
105
+ }
106
+
107
+ console.log(`Dashboard: ${dashboardUrl}`);
108
+
109
+ const opener = platform() === 'win32' ? 'explorer' : platform() === 'darwin' ? 'open' : 'xdg-open';
110
+ try {
111
+ await run(opener, [dashboardUrl]);
112
+ success('Opened dashboard in browser');
113
+ } catch {
114
+ info('Could not open browser automatically. Visit the URL above.');
115
+ }
116
+ });
117
+
72
118
  program
73
119
  .command('status')
74
120
  .description('Quick status summary (service + tunnel + doctor-lite)')
@@ -6,7 +6,6 @@ import {
6
6
  EXIT,
7
7
  } from '../constants.js';
8
8
  import { CliError } from '../errors.js';
9
- import { inferDomainFromRedirect } from '../env-file.js';
10
9
  import { loadHiveConfig, updateHiveConfig } from '../config.js';
11
10
  import { validateDomain } from '../checks.js';
12
11
  import { run } from '../exec.js';
@@ -23,7 +22,6 @@ import {
23
22
  parseInteger,
24
23
  requiredOrFallback,
25
24
  resolveContext,
26
- setRedirectUriForDomain,
27
25
  } from '../core/context.js';
28
26
 
29
27
  export function registerTunnelCommands(program) {
@@ -47,10 +45,7 @@ export function registerTunnelCommands(program) {
47
45
  throw new CliError('Fix env file first (hive env check)', EXIT.FAIL);
48
46
  }
49
47
 
50
- const domain = requiredOrFallback(
51
- options.domain,
52
- inferDomainFromRedirect(env.DISCORD_REDIRECT_URI) || config.domain
53
- );
48
+ const domain = requiredOrFallback(options.domain, config.domain);
54
49
  if (!validateDomain(domain)) {
55
50
  throw new CliError(`Invalid domain: ${domain}`);
56
51
  }
@@ -72,13 +67,6 @@ export function registerTunnelCommands(program) {
72
67
  installService: Boolean(options.installService),
73
68
  });
74
69
 
75
- const nextEnv = await setRedirectUriForDomain({
76
- envFile,
77
- env,
78
- domain,
79
- yes: Boolean(options.yes),
80
- });
81
-
82
70
  await updateHiveConfig({
83
71
  envFile,
84
72
  domain,
@@ -88,9 +76,6 @@ export function registerTunnelCommands(program) {
88
76
  cloudflaredConfigFile,
89
77
  });
90
78
 
91
- if (nextEnv.DISCORD_REDIRECT_URI !== env.DISCORD_REDIRECT_URI) {
92
- success('Redirect URI synced for tunnel domain');
93
- }
94
79
  success('Tunnel setup complete');
95
80
  });
96
81
 
package/cli/constants.js CHANGED
@@ -1,24 +1,15 @@
1
1
  import { homedir } from 'os';
2
- import { dirname, join } from 'path';
3
- import { fileURLToPath } from 'url';
2
+ import { join } from 'path';
4
3
 
5
- const __dirname = dirname(fileURLToPath(import.meta.url));
6
-
7
- export const SERVER_ROOT = join(__dirname, '..');
8
4
  export const HIVE_HOME = join(homedir(), '.hive');
9
5
  export const HIVE_CONFIG_FILE = join(HIVE_HOME, 'config.json');
10
6
  export const DEFAULT_ENV_FILE = join(HIVE_HOME, 'server', '.env');
11
- export const LEGACY_ENV_FILE = join(SERVER_ROOT, '.env');
12
7
  export const DEFAULT_DOMAIN = 'collab.example.com';
13
8
  export const DEFAULT_TUNNEL_NAME = 'hive';
14
9
  export const DEFAULT_CLOUDFLARED_CONFIG = join(homedir(), '.cloudflared', 'config.yml');
15
10
  export const DEFAULT_CLOUDFLARED_CERT = join(homedir(), '.cloudflared', 'cert.pem');
16
11
 
17
12
  export const REQUIRED_ENV_KEYS = [
18
- 'DISCORD_CLIENT_ID',
19
- 'DISCORD_CLIENT_SECRET',
20
- 'DISCORD_REDIRECT_URI',
21
- 'OWNER_DISCORD_ID',
22
13
  'JWT_SECRET',
23
14
  'VAULT_PATH',
24
15
  'PORT',
@@ -6,8 +6,7 @@ import prompts from 'prompts';
6
6
  import { DEFAULT_ENV_FILE, EXIT } from '../constants.js';
7
7
  import { CliError } from '../errors.js';
8
8
  import { loadHiveConfig } from '../config.js';
9
- import { loadEnvFile, normalizeEnv, validateEnvValues, writeEnvFile } from '../env-file.js';
10
- import { success } from '../output.js';
9
+ import { loadEnvFile, normalizeEnv, validateEnvValues } from '../env-file.js';
11
10
  import { getServiceDefaults } from '../service.js';
12
11
 
13
12
  export function parseInteger(value, key) {
@@ -80,31 +79,13 @@ export function assertEnvFileExists(envFile) {
80
79
  }
81
80
  }
82
81
 
83
- export async function loadValidatedEnv(envFile, { requireFile = true } = {}) {
82
+ export async function loadValidatedEnv(envFile, { requireFile = true, requireVaultPath = true } = {}) {
84
83
  if (requireFile) {
85
84
  assertEnvFileExists(envFile);
86
85
  }
87
86
 
88
87
  const raw = await loadEnvFile(envFile);
89
88
  const env = normalizeEnv(raw);
90
- const issues = validateEnvValues(env);
89
+ const issues = validateEnvValues(env, { requireVaultPath });
91
90
  return { env, issues };
92
91
  }
93
-
94
- export async function setRedirectUriForDomain({ envFile, env, domain, yes = false }) {
95
- const expected = `https://${domain}/auth/callback`;
96
- if (env.DISCORD_REDIRECT_URI === expected) return env;
97
-
98
- const shouldUpdate = await promptConfirm(
99
- `Set DISCORD_REDIRECT_URI to ${expected}?`,
100
- yes,
101
- true
102
- );
103
-
104
- if (!shouldUpdate) return env;
105
-
106
- const next = { ...env, DISCORD_REDIRECT_URI: expected };
107
- await writeEnvFile(envFile, next);
108
- success(`Updated DISCORD_REDIRECT_URI -> ${expected}`);
109
- return next;
110
- }
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,19 +58,12 @@ 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
  }
@@ -78,15 +72,6 @@ export function validateEnvValues(values) {
78
72
  const port = parseInt(values.PORT ?? '', 10);
79
73
  if (!Number.isInteger(port) || port <= 0) issues.push('PORT must be a positive integer');
80
74
 
81
- try {
82
- const uri = new URL(values.DISCORD_REDIRECT_URI ?? '');
83
- if (!/^https?:$/.test(uri.protocol)) {
84
- issues.push('DISCORD_REDIRECT_URI must use http or https');
85
- }
86
- } catch {
87
- issues.push('DISCORD_REDIRECT_URI must be a valid URL');
88
- }
89
-
90
75
  return issues;
91
76
  }
92
77
 
@@ -101,7 +86,14 @@ export async function ensureVaultPathReadable(pathValue) {
101
86
  }
102
87
 
103
88
  export async function promptForEnv({ envFile, existing, yes = false, preset = {} }) {
104
- 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);
105
97
 
106
98
  if (yes) {
107
99
  await writeEnvFile(envFile, current);
@@ -109,19 +101,13 @@ export async function promptForEnv({ envFile, existing, yes = false, preset = {}
109
101
  }
110
102
 
111
103
  const questions = [
112
- { name: 'DISCORD_CLIENT_ID', message: 'Discord Client ID' },
113
- { name: 'DISCORD_CLIENT_SECRET', message: 'Discord Client Secret', secret: true },
114
- { name: 'OWNER_DISCORD_ID', message: 'Managed vault owner Discord ID' },
115
- { name: 'JWT_SECRET', message: 'JWT secret', secret: true },
116
- { name: 'VAULT_PATH', message: 'Vault absolute path' },
117
104
  { name: 'PORT', message: 'HTTP port' },
118
- { name: 'DISCORD_REDIRECT_URI', message: 'Discord redirect URI' },
119
105
  ];
120
106
 
121
107
  const answers = {};
122
108
  for (const q of questions) {
123
109
  const response = await prompts({
124
- type: q.secret ? 'password' : 'text',
110
+ type: 'text',
125
111
  name: 'value',
126
112
  message: q.message,
127
113
  initial: current[q.name] ?? '',