@fyresmith/hive-server 4.0.0 → 4.0.1

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/constants.js CHANGED
@@ -18,17 +18,14 @@ export const REQUIRED_ENV_KEYS = [
18
18
  'DISCORD_CLIENT_ID',
19
19
  'DISCORD_CLIENT_SECRET',
20
20
  'DISCORD_REDIRECT_URI',
21
- 'DISCORD_GUILD_ID',
22
21
  'OWNER_DISCORD_ID',
23
22
  'JWT_SECRET',
24
23
  'VAULT_PATH',
25
24
  'PORT',
26
- 'YJS_PORT',
27
25
  ];
28
26
 
29
27
  export const DEFAULT_ENV_VALUES = {
30
28
  PORT: '3000',
31
- YJS_PORT: '3001',
32
29
  };
33
30
 
34
31
  export const DEFAULT_CONFIG = {
package/cli/env-file.js CHANGED
@@ -76,9 +76,7 @@ export function validateEnvValues(values) {
76
76
  }
77
77
 
78
78
  const port = parseInt(values.PORT ?? '', 10);
79
- const yjsPort = parseInt(values.YJS_PORT ?? '', 10);
80
79
  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
80
 
83
81
  try {
84
82
  const uri = new URL(values.DISCORD_REDIRECT_URI ?? '');
@@ -113,12 +111,10 @@ export async function promptForEnv({ envFile, existing, yes = false, preset = {}
113
111
  const questions = [
114
112
  { name: 'DISCORD_CLIENT_ID', message: 'Discord Client ID' },
115
113
  { name: 'DISCORD_CLIENT_SECRET', message: 'Discord Client Secret', secret: true },
116
- { name: 'DISCORD_GUILD_ID', message: 'Discord Guild ID' },
117
114
  { name: 'OWNER_DISCORD_ID', message: 'Managed vault owner Discord ID' },
118
115
  { name: 'JWT_SECRET', message: 'JWT secret', secret: true },
119
116
  { name: 'VAULT_PATH', message: 'Vault absolute path' },
120
117
  { name: 'PORT', message: 'HTTP port' },
121
- { name: 'YJS_PORT', message: 'Yjs WS port' },
122
118
  { name: 'DISCORD_REDIRECT_URI', message: 'Discord redirect URI' },
123
119
  ];
124
120
 
@@ -95,7 +95,6 @@ export async function runSetupWizard(options) {
95
95
  const shouldSetupTunnel = await promptConfirm('Configure Cloudflare Tunnel now?', yes, true);
96
96
  if (shouldSetupTunnel) {
97
97
  const port = parseInteger(envValues.PORT, 'PORT');
98
- const yjsPort = parseInteger(envValues.YJS_PORT, 'YJS_PORT');
99
98
  const tunnelName = requiredOrFallback(options.tunnelName, nextConfig.tunnelName || DEFAULT_TUNNEL_NAME);
100
99
  const cloudflaredConfigFile = requiredOrFallback(
101
100
  options.cloudflaredConfigFile,
@@ -109,7 +108,6 @@ export async function runSetupWizard(options) {
109
108
  configFile: cloudflaredConfigFile,
110
109
  certPath: DEFAULT_CLOUDFLARED_CERT,
111
110
  port,
112
- yjsPort,
113
111
  yes,
114
112
  installService: tunnelService,
115
113
  });
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
@@ -16,7 +16,6 @@ const REQUIRED = [
16
16
  'DISCORD_CLIENT_ID',
17
17
  'DISCORD_CLIENT_SECRET',
18
18
  'DISCORD_REDIRECT_URI',
19
- 'DISCORD_GUILD_ID',
20
19
  'OWNER_DISCORD_ID',
21
20
  ];
22
21
 
@@ -28,13 +27,9 @@ function validateEnv() {
28
27
  }
29
28
 
30
29
  const port = parseInt(process.env.PORT ?? '3000', 10);
31
- const yjsPort = parseInt(process.env.YJS_PORT ?? '3001', 10);
32
30
  if (!Number.isInteger(port) || port <= 0) {
33
31
  throw new Error('[startup] PORT must be a positive integer');
34
32
  }
35
- if (!Number.isInteger(yjsPort) || yjsPort <= 0) {
36
- throw new Error('[startup] YJS_PORT must be a positive integer');
37
- }
38
33
 
39
34
  return { port };
40
35
  }
@@ -68,6 +63,18 @@ export async function startHiveServer(options = {}) {
68
63
 
69
64
  app.use(express.json());
70
65
 
66
+ // Allow desktop plugin fetch calls (Authorization header triggers preflight).
67
+ app.use((req, res, next) => {
68
+ res.setHeader('Access-Control-Allow-Origin', '*');
69
+ res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
70
+ res.setHeader('Access-Control-Allow-Headers', 'Authorization,Content-Type');
71
+ if (req.method === 'OPTIONS') {
72
+ res.sendStatus(204);
73
+ return;
74
+ }
75
+ next();
76
+ });
77
+
71
78
  // Auth routes
72
79
  app.use('/auth', authRoutes);
73
80
  app.use('/managed', managedRoutes);
@@ -85,7 +92,17 @@ export async function startHiveServer(options = {}) {
85
92
  }
86
93
 
87
94
  attachHandlers(io, getActiveRooms, broadcastFileUpdated, forceCloseRoom);
88
- startYjsServer(broadcastFileUpdated);
95
+ const yjsWss = startYjsServer(httpServer, broadcastFileUpdated);
96
+
97
+ httpServer.on('upgrade', (req, socket, head) => {
98
+ const { pathname } = new URL(req.url, 'http://localhost');
99
+ if (pathname.startsWith('/yjs')) {
100
+ yjsWss.handleUpgrade(req, socket, head, (ws) => {
101
+ yjsWss.emit('connection', ws, req);
102
+ });
103
+ }
104
+ // Socket.IO handles /socket.io upgrades automatically via its own listener
105
+ });
89
106
 
90
107
  // Chokidar watch for external (non-plugin) changes
91
108
  vault.initWatcher((relPath, event) => {
package/lib/yjsServer.js CHANGED
@@ -204,10 +204,9 @@ export async function forceCloseRoom(relPath) {
204
204
  await closeRoom(docName, { closeClients: true, reason: 'forced' });
205
205
  }
206
206
 
207
- export function startYjsServer(broadcastFileUpdated) {
207
+ export function startYjsServer(httpServer, broadcastFileUpdated) {
208
208
  broadcastRef = broadcastFileUpdated;
209
- const port = parseInt(process.env.YJS_PORT ?? '3001', 10);
210
- const wss = new WebSocketServer({ port });
209
+ const wss = new WebSocketServer({ noServer: true });
211
210
 
212
211
  wss.on('connection', async (conn, req) => {
213
212
  const url = new URL(req.url, 'http://localhost');
@@ -274,6 +273,5 @@ export function startYjsServer(broadcastFileUpdated) {
274
273
  }
275
274
  });
276
275
 
277
- console.log(`[yjs] WebSocket server listening on port ${port}`);
278
276
  return wss;
279
277
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fyresmith/hive-server",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "type": "module",
5
5
  "description": "Collaborative Obsidian vault server",
6
6
  "main": "index.js",
package/routes/auth.js CHANGED
@@ -29,9 +29,10 @@ router.get('/login', (req, res) => {
29
29
  const state = randomBytes(16).toString('hex');
30
30
  stateMap.set(state, { ts: Date.now() });
31
31
 
32
+ const scope = process.env.DISCORD_GUILD_ID ? ['identify', 'guilds'] : ['identify'];
32
33
  const url = oauth.generateAuthUrl({
33
34
  clientId: process.env.DISCORD_CLIENT_ID,
34
- scope: ['identify', 'guilds'],
35
+ scope,
35
36
  redirectUri: process.env.DISCORD_REDIRECT_URI,
36
37
  state,
37
38
  });
@@ -56,22 +57,25 @@ router.get('/callback', async (req, res) => {
56
57
 
57
58
  try {
58
59
  // Exchange code for access token
60
+ const scope = process.env.DISCORD_GUILD_ID ? ['identify', 'guilds'] : ['identify'];
59
61
  const tokenData = await oauth.tokenRequest({
60
62
  clientId: process.env.DISCORD_CLIENT_ID,
61
63
  clientSecret: process.env.DISCORD_CLIENT_SECRET,
62
64
  redirectUri: process.env.DISCORD_REDIRECT_URI,
63
65
  code,
64
- scope: ['identify', 'guilds'],
66
+ scope,
65
67
  grantType: 'authorization_code',
66
68
  });
67
69
 
68
70
  const accessToken = tokenData.access_token;
69
71
 
70
- // Verify guild membership
71
- const guilds = await oauth.getUserGuilds(accessToken);
72
- const isMember = guilds.some((g) => g.id === process.env.DISCORD_GUILD_ID);
73
- if (!isMember) {
74
- return res.status(403).send('Access denied: you are not a member of the required Discord server.');
72
+ // Verify guild membership (only when DISCORD_GUILD_ID is configured)
73
+ if (process.env.DISCORD_GUILD_ID) {
74
+ const guilds = await oauth.getUserGuilds(accessToken);
75
+ const isMember = guilds.some((g) => g.id === process.env.DISCORD_GUILD_ID);
76
+ if (!isMember) {
77
+ return res.status(403).send('Access denied: you are not a member of the required Discord server.');
78
+ }
75
79
  }
76
80
 
77
81
  // Fetch user profile
package/routes/managed.js CHANGED
@@ -13,6 +13,17 @@ import {
13
13
 
14
14
  const router = Router();
15
15
 
16
+ router.use((req, res, next) => {
17
+ res.setHeader('Access-Control-Allow-Origin', '*');
18
+ res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
19
+ res.setHeader('Access-Control-Allow-Headers', 'Authorization,Content-Type');
20
+ if (req.method === 'OPTIONS') {
21
+ res.sendStatus(204);
22
+ return;
23
+ }
24
+ next();
25
+ });
26
+
16
27
  function getVaultPath() {
17
28
  const value = String(process.env.VAULT_PATH ?? '').trim();
18
29
  if (!value) throw new Error('VAULT_PATH env var is required');