tao-shen commited on
Commit
bcc6655
·
verified ·
1 Parent(s): 8bd6f75

Upload scripts/token-redirect.cjs with huggingface_hub

Browse files
Files changed (1) hide show
  1. scripts/token-redirect.cjs +225 -14
scripts/token-redirect.cjs CHANGED
@@ -1,37 +1,248 @@
1
  /**
2
- * token-redirect.cjs — Node.js preload script
3
- *
4
- * Intercepts HTTP requests to the root URL "/" and redirects to
5
- * "/?token=GATEWAY_TOKEN" so the Control UI auto-fills the gateway token.
6
  *
7
  * Loaded via NODE_OPTIONS --require before OpenClaw starts.
 
 
 
 
 
 
8
  */
9
  'use strict';
10
 
11
  const http = require('http');
 
 
 
12
 
13
  const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || 'huggingclaw';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  const origEmit = http.Server.prototype.emit;
15
 
16
  http.Server.prototype.emit = function (event, ...args) {
17
  if (event === 'request') {
18
  const [req, res] = args;
19
- // Only redirect normal GET to "/" without token — skip WebSocket upgrades
20
- if (req.method === 'GET' && !req.headers.upgrade) {
21
- try {
22
- const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
23
- if (url.pathname === '/' && !url.searchParams.has('token')) {
24
- url.searchParams.set('token', GATEWAY_TOKEN);
25
- res.writeHead(302, { Location: url.pathname + url.search });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  res.end();
27
  return true;
28
  }
29
- } catch (_) {
30
- // URL parse error — pass through
31
  }
32
  }
33
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  return origEmit.apply(this, [event, ...args]);
35
  };
36
 
37
- console.log('[token-redirect] Gateway token redirect active');
 
 
 
 
 
1
  /**
2
+ * token-redirect.cjs — Node.js preload script (enhanced)
 
 
 
3
  *
4
  * Loaded via NODE_OPTIONS --require before OpenClaw starts.
5
+ * Intercepts OpenClaw's HTTP server to:
6
+ * 1. Redirect GET / to /?token=GATEWAY_TOKEN (auto-fill token)
7
+ * 2. Proxy A2A requests (/.well-known/*, /a2a/*) to gateway port 18800
8
+ * 3. Serve /api/state and /agents for Office frontends
9
+ * 4. Fix iframe embedding (strip X-Frame-Options, fix CSP)
10
+ * 5. Serve Office frontend when OFFICE_MODE=1
11
  */
12
  'use strict';
13
 
14
  const http = require('http');
15
+ const url = require('url');
16
+ const fs = require('fs');
17
+ const path = require('path');
18
 
19
  const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || 'huggingclaw';
20
+ const AGENT_NAME = process.env.AGENT_NAME || 'HuggingClaw';
21
+ const A2A_PORT = 18800;
22
+ const OFFICE_MODE = process.env.OFFICE_MODE === '1';
23
+
24
+ // Frontend directory for Office mode
25
+ const FRONTEND_DIR = fs.existsSync('/home/node/frontend')
26
+ ? '/home/node/frontend'
27
+ : path.join(__dirname, '..', 'frontend');
28
+
29
+ const MIME_TYPES = {
30
+ '.html': 'text/html; charset=utf-8',
31
+ '.js': 'application/javascript',
32
+ '.css': 'text/css',
33
+ '.json': 'application/json',
34
+ '.png': 'image/png',
35
+ '.jpg': 'image/jpeg',
36
+ '.jpeg': 'image/jpeg',
37
+ '.webp': 'image/webp',
38
+ '.gif': 'image/gif',
39
+ '.svg': 'image/svg+xml',
40
+ '.woff2': 'font/woff2',
41
+ '.woff': 'font/woff',
42
+ '.ttf': 'font/ttf',
43
+ '.ico': 'image/x-icon',
44
+ '.mp3': 'audio/mpeg',
45
+ '.ogg': 'audio/ogg',
46
+ '.md': 'text/markdown; charset=utf-8',
47
+ };
48
+
49
+ function serveStaticFile(res, filePath) {
50
+ const resolved = path.resolve(filePath);
51
+ if (!resolved.startsWith(path.resolve(FRONTEND_DIR))) {
52
+ res.writeHead(403);
53
+ return res.end('Forbidden');
54
+ }
55
+ fs.readFile(resolved, (err, data) => {
56
+ if (err) {
57
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
58
+ return res.end('Not Found');
59
+ }
60
+ const ext = path.extname(resolved).toLowerCase();
61
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
62
+ res.writeHead(200, {
63
+ 'Content-Type': contentType,
64
+ 'Cache-Control': (ext === '.html') ? 'no-cache' : 'public, max-age=86400',
65
+ 'Access-Control-Allow-Origin': '*'
66
+ });
67
+ res.end(data);
68
+ });
69
+ }
70
+
71
+ // Remote agents polling
72
+ const REMOTE_AGENTS_RAW = process.env.REMOTE_AGENTS || '';
73
+ const remoteAgents = REMOTE_AGENTS_RAW
74
+ ? REMOTE_AGENTS_RAW.split(',').map(entry => {
75
+ const [id, name, baseUrl] = entry.trim().split('|');
76
+ return { id, name, baseUrl };
77
+ }).filter(a => a.id && a.name && a.baseUrl)
78
+ : [];
79
+
80
+ const remoteAgentStates = new Map();
81
+
82
+ async function pollRemoteAgent(agent) {
83
+ try {
84
+ const controller = new AbortController();
85
+ const timeout = setTimeout(() => controller.abort(), 5000);
86
+ const resp = await fetch(`${agent.baseUrl}/api/state`, { signal: controller.signal });
87
+ clearTimeout(timeout);
88
+ if (resp.ok) {
89
+ const data = await resp.json();
90
+ remoteAgentStates.set(agent.id, {
91
+ agentId: agent.id, name: agent.name,
92
+ state: data.state || 'idle',
93
+ detail: data.detail || '',
94
+ area: (data.state === 'idle') ? 'breakroom' : (data.state === 'error') ? 'error' : 'writing',
95
+ authStatus: 'approved',
96
+ updated_at: data.updated_at
97
+ });
98
+ }
99
+ } catch (_) {
100
+ if (!remoteAgentStates.has(agent.id)) {
101
+ remoteAgentStates.set(agent.id, {
102
+ agentId: agent.id, name: agent.name,
103
+ state: 'syncing', detail: `${agent.name} is starting...`,
104
+ area: 'door', authStatus: 'approved'
105
+ });
106
+ }
107
+ }
108
+ }
109
+
110
+ if (remoteAgents.length > 0) {
111
+ setInterval(() => remoteAgents.forEach(a => pollRemoteAgent(a)), 5000);
112
+ remoteAgents.forEach(a => pollRemoteAgent(a));
113
+ console.log(`[token-redirect] Monitoring ${remoteAgents.length} remote agent(s)`);
114
+ }
115
+
116
+ // State tracking
117
+ let currentState = {
118
+ state: 'syncing', detail: `${AGENT_NAME} is starting...`,
119
+ progress: 0, updated_at: new Date().toISOString()
120
+ };
121
+
122
+ // Once OpenClaw starts listening, mark as idle
123
+ setTimeout(() => {
124
+ if (currentState.state === 'syncing') {
125
+ currentState = {
126
+ state: 'idle', detail: `${AGENT_NAME} is running`,
127
+ progress: 100, updated_at: new Date().toISOString()
128
+ };
129
+ }
130
+ }, 30000);
131
+
132
+ function proxyToA2A(req, res) {
133
+ const options = {
134
+ hostname: '127.0.0.1', port: A2A_PORT,
135
+ path: req.url, method: req.method,
136
+ headers: { ...req.headers, host: `127.0.0.1:${A2A_PORT}` }
137
+ };
138
+ const proxy = http.request(options, (proxyRes) => {
139
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
140
+ proxyRes.pipe(res, { end: true });
141
+ });
142
+ proxy.on('error', () => {
143
+ if (!res.headersSent) {
144
+ res.writeHead(502, { 'Content-Type': 'application/json' });
145
+ res.end(JSON.stringify({ error: 'A2A gateway unavailable' }));
146
+ }
147
+ });
148
+ req.pipe(proxy, { end: true });
149
+ }
150
+
151
  const origEmit = http.Server.prototype.emit;
152
 
153
  http.Server.prototype.emit = function (event, ...args) {
154
  if (event === 'request') {
155
  const [req, res] = args;
156
+ const parsed = url.parse(req.url, true);
157
+ const pathname = parsed.pathname;
158
+
159
+ // A2A routes proxy to A2A gateway on 18800
160
+ if (pathname.startsWith('/.well-known/') || pathname.startsWith('/a2a/')) {
161
+ proxyToA2A(req, res);
162
+ return true;
163
+ }
164
+
165
+ // /api/state → return current state
166
+ if (pathname === '/api/state' || pathname === '/status') {
167
+ // Update state to idle once we're handling requests
168
+ if (currentState.state === 'syncing') {
169
+ currentState = {
170
+ state: 'idle', detail: `${AGENT_NAME} is running`,
171
+ progress: 100, updated_at: new Date().toISOString()
172
+ };
173
+ }
174
+ res.writeHead(200, {
175
+ 'Content-Type': 'application/json',
176
+ 'Access-Control-Allow-Origin': '*'
177
+ });
178
+ res.end(JSON.stringify({
179
+ ...currentState,
180
+ officeName: `${AGENT_NAME}'s Office`
181
+ }));
182
+ return true;
183
+ }
184
+
185
+ // /agents → return remote agent list
186
+ if (pathname === '/agents' && req.method === 'GET') {
187
+ res.writeHead(200, {
188
+ 'Content-Type': 'application/json',
189
+ 'Access-Control-Allow-Origin': '*'
190
+ });
191
+ res.end(JSON.stringify([...remoteAgentStates.values()]));
192
+ return true;
193
+ }
194
+
195
+ // Office mode: serve frontend at /, static at /static/*, admin proxies to OpenClaw
196
+ if (OFFICE_MODE) {
197
+ if (pathname === '/' && req.method === 'GET' && !req.headers.upgrade) {
198
+ serveStaticFile(res, path.join(FRONTEND_DIR, 'index.html'));
199
+ return true;
200
+ }
201
+ if (pathname.startsWith('/static/')) {
202
+ serveStaticFile(res, path.join(FRONTEND_DIR, pathname.slice('/static/'.length).split('?')[0]));
203
+ return true;
204
+ }
205
+ if (pathname === '/admin' || pathname === '/admin/') {
206
+ // Rewrite to root with token and let OpenClaw handle it
207
+ req.url = GATEWAY_TOKEN ? `/?token=${GATEWAY_TOKEN}` : '/';
208
+ return origEmit.apply(this, [event, ...args]);
209
+ }
210
+ } else {
211
+ // Default mode: redirect GET / to /?token=xxx
212
+ if (req.method === 'GET' && !req.headers.upgrade) {
213
+ if (pathname === '/' && !parsed.query.token) {
214
+ res.writeHead(302, { Location: `/?token=${GATEWAY_TOKEN}` });
215
  res.end();
216
  return true;
217
  }
 
 
218
  }
219
  }
220
  }
221
+
222
+ // Fix iframe embedding on all responses
223
+ if (event === 'request') {
224
+ const [req, res] = args;
225
+ const origWriteHead = res.writeHead;
226
+ res.writeHead = function (statusCode, ...whArgs) {
227
+ // Remove X-Frame-Options to allow HF iframe embedding
228
+ if (res.getHeader) {
229
+ res.removeHeader('x-frame-options');
230
+ const csp = res.getHeader('content-security-policy');
231
+ if (csp && typeof csp === 'string') {
232
+ res.setHeader('content-security-policy',
233
+ csp.replace(/frame-ancestors\s+'none'/i,
234
+ "frame-ancestors 'self' https://huggingface.co https://*.hf.space"));
235
+ }
236
+ }
237
+ return origWriteHead.apply(this, [statusCode, ...whArgs]);
238
+ };
239
+ }
240
+
241
  return origEmit.apply(this, [event, ...args]);
242
  };
243
 
244
+ // Also handle WebSocket upgrades for A2A
245
+ const origServerEmit = http.Server.prototype.emit;
246
+ // Already patched above, A2A WS upgrades handled via 'upgrade' event in OpenClaw
247
+
248
+ console.log(`[token-redirect] Active: token=${GATEWAY_TOKEN}, agent=${AGENT_NAME}, office=${OFFICE_MODE}`);