@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 +0 -3
- package/cli/env-file.js +0 -4
- package/cli/flows/setup.js +0 -2
- package/cli/tunnel.js +1 -4
- package/index.js +23 -6
- package/lib/yjsServer.js +2 -4
- package/package.json +1 -1
- package/routes/auth.js +11 -7
- package/routes/managed.js +11 -0
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
|
|
package/cli/flows/setup.js
CHANGED
|
@@ -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
|
|
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
|
|
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
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
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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');
|