dsh-mesh-node 0.0.1 → 0.0.2
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/package.json +1 -1
- package/src/core/cluster.js +341 -340
- package/src/core/sakura-api.js +103 -0
- package/src/core/tunnels.js +212 -0
- package/src/server.js +317 -313
package/src/server.js
CHANGED
|
@@ -1,313 +1,317 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
-
import { createServer as createHttpServer } from 'node:http'
|
|
3
|
-
import { createServer as createHttpsServer } from 'node:https'
|
|
4
|
-
import { McpServer, createMcpHandler } from '@modelcontextprotocol/server'
|
|
5
|
-
import { toNodeHandler } from '@modelcontextprotocol/node'
|
|
6
|
-
import { MESH_VERSION, verifyBearer, createConfigLoader, createSecretsLoader, meshHome, addInboundToken, peerAclForToken } from './core/config.js'
|
|
7
|
-
import { makeAudit, auditPath } from './core/audit.js'
|
|
8
|
-
import { JobRunner } from './core/jobs.js'
|
|
9
|
-
import { loadTlsMaterial, describeCert } from './core/tls.js'
|
|
10
|
-
import { claimPairingCode, readPairingState } from './core/pairing.js'
|
|
11
|
-
import { dispatch, isAllowed, applyPeerAcl, registerTools, toolNames } from './tools/index.js'
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
res.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
.replace(
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
*
|
|
80
|
-
* @param {object}
|
|
81
|
-
* @param {object} opts.
|
|
82
|
-
* @param {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
import { createServer as createHttpServer } from 'node:http'
|
|
3
|
+
import { createServer as createHttpsServer } from 'node:https'
|
|
4
|
+
import { McpServer, createMcpHandler } from '@modelcontextprotocol/server'
|
|
5
|
+
import { toNodeHandler } from '@modelcontextprotocol/node'
|
|
6
|
+
import { MESH_VERSION, verifyBearer, createConfigLoader, createSecretsLoader, meshHome, addInboundToken, peerAclForToken } from './core/config.js'
|
|
7
|
+
import { makeAudit, auditPath } from './core/audit.js'
|
|
8
|
+
import { JobRunner } from './core/jobs.js'
|
|
9
|
+
import { loadTlsMaterial, describeCert } from './core/tls.js'
|
|
10
|
+
import { claimPairingCode, readPairingState } from './core/pairing.js'
|
|
11
|
+
import { dispatch, isAllowed, applyPeerAcl, registerTools, toolNames } from './tools/index.js'
|
|
12
|
+
import { createTunnelCache } from './core/tunnels.js'
|
|
13
|
+
|
|
14
|
+
const NODE_STARTED_AT = new Date().toISOString()
|
|
15
|
+
|
|
16
|
+
const INSTRUCTIONS = `This endpoint is a dsh-mesh node: another developer machine exposed over the Model Context Protocol.
|
|
17
|
+
Call mesh_self first to learn what the peer can do and which tools your token may use.
|
|
18
|
+
All paths are paths on the PEER machine, not on yours.
|
|
19
|
+
Long operations: use task_submit (with an idem key) then poll task_status, instead of risking a call timeout.
|
|
20
|
+
shell_run is opt-in and normally refused.`
|
|
21
|
+
|
|
22
|
+
/** Carries the authenticated token id down into the per-request MCP server factory. */
|
|
23
|
+
const requestScope = new AsyncLocalStorage()
|
|
24
|
+
|
|
25
|
+
export function buildMcpServer({ ctx, log = console }) {
|
|
26
|
+
const config = ctx.getConfig()
|
|
27
|
+
const server = new McpServer(
|
|
28
|
+
{ name: `dsh-mesh/${config.node?.name || 'local'}`, version: MESH_VERSION },
|
|
29
|
+
{ instructions: INSTRUCTIONS }
|
|
30
|
+
)
|
|
31
|
+
registerTools(server, ctx)
|
|
32
|
+
return server
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sendJson(res, status, body, extraHeaders = {}) {
|
|
36
|
+
const text = JSON.stringify(body)
|
|
37
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', ...extraHeaders })
|
|
38
|
+
res.end(text)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Read a small JSON body (pairing requests are tiny; anything huge is a mistake). */
|
|
42
|
+
function readJsonBody(req, limitBytes = 8192) {
|
|
43
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
44
|
+
let size = 0
|
|
45
|
+
const chunks = []
|
|
46
|
+
req.on('data', (chunk) => {
|
|
47
|
+
size += chunk.length
|
|
48
|
+
if (size > limitBytes) {
|
|
49
|
+
rejectPromise(new Error('body too large'))
|
|
50
|
+
req.destroy()
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
chunks.push(chunk)
|
|
54
|
+
})
|
|
55
|
+
req.on('end', () => {
|
|
56
|
+
const text = Buffer.concat(chunks).toString('utf8').replace(/^\uFEFF/, '').trim()
|
|
57
|
+
if (text === '') return resolvePromise({})
|
|
58
|
+
try {
|
|
59
|
+
resolvePromise(JSON.parse(text))
|
|
60
|
+
} catch {
|
|
61
|
+
rejectPromise(new Error('body is not valid JSON'))
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
req.on('error', rejectPromise)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Peer ids double as token ids; keep them filename/log friendly. */
|
|
69
|
+
function sanitizePeerId(raw) {
|
|
70
|
+
const id = String(raw || '')
|
|
71
|
+
.trim()
|
|
72
|
+
.replace(/[^A-Za-z0-9._-]/g, '-')
|
|
73
|
+
.replace(/-+/g, '-')
|
|
74
|
+
.slice(0, 48)
|
|
75
|
+
return id === '' ? null : id
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Start the mesh HTTP endpoint.
|
|
80
|
+
* @param {object} opts
|
|
81
|
+
* @param {object} [opts.config] Fixed config (tests). Omit to load and hot-reload from disk.
|
|
82
|
+
* @param {object} opts.secrets Inbound tokens.
|
|
83
|
+
* @param {string} [opts.home] Mesh home directory (defaults to ~/.dsh-mesh).
|
|
84
|
+
*/
|
|
85
|
+
export async function startMeshServer({ config, secrets, log = console, port, bind, tls, home } = {}) {
|
|
86
|
+
const meshHomeDir = home || meshHome()
|
|
87
|
+
const getConfig = config ? () => config : createConfigLoader({ home: meshHomeDir, log })
|
|
88
|
+
// Secrets hot-reload too: rotating a token takes effect without a restart.
|
|
89
|
+
const getSecrets = secrets ? () => secrets : createSecretsLoader({ home: meshHomeDir, log })
|
|
90
|
+
const audit = makeAudit({ home: meshHomeDir, log })
|
|
91
|
+
|
|
92
|
+
const initial = getConfig()
|
|
93
|
+
// 2026-11-09:隧道状态缓存(自建 frps + Sakura)。/health 携带,60s 后台刷新,不阻塞请求。
|
|
94
|
+
const tunnelCache = createTunnelCache({ repo: initial.repo || process.cwd(), home: meshHomeDir, refreshMs: Math.max(3000, Number(process.env.TUNNEL_CACHE_MS || 10_000)), log }).start()
|
|
95
|
+
const jobs = new JobRunner({
|
|
96
|
+
home: meshHomeDir,
|
|
97
|
+
audit,
|
|
98
|
+
log,
|
|
99
|
+
limits: initial.limits || {},
|
|
100
|
+
exec: (tool, args, opts) => dispatch(ctx, tool, args, opts)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Shared context. `peerId` is filled per request from the verified bearer token.
|
|
104
|
+
const ctx = {
|
|
105
|
+
home: meshHomeDir,
|
|
106
|
+
secrets,
|
|
107
|
+
getSecrets,
|
|
108
|
+
log,
|
|
109
|
+
audit,
|
|
110
|
+
jobs,
|
|
111
|
+
getConfig,
|
|
112
|
+
isAllowed,
|
|
113
|
+
toolNames: () => toolNames(ctx),
|
|
114
|
+
dispatch: (name, args, opts) => dispatch(ctx, name, args, opts)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const nodeCfg = initial.node || {}
|
|
118
|
+
const listenPort = port ?? nodeCfg.port ?? 8124
|
|
119
|
+
const listenBind = bind ?? nodeCfg.bind ?? '127.0.0.1'
|
|
120
|
+
const mcpPath = nodeCfg.path || '/mcp'
|
|
121
|
+
const tlsCfg = tls ?? nodeCfg.tls ?? { enabled: false }
|
|
122
|
+
|
|
123
|
+
let calls = 0
|
|
124
|
+
const mcpHandler = createMcpHandler(
|
|
125
|
+
() => {
|
|
126
|
+
const scope = requestScope.getStore()
|
|
127
|
+
const peerAcl = scope?.peerAcl || null
|
|
128
|
+
return buildMcpServer({
|
|
129
|
+
ctx: {
|
|
130
|
+
...ctx,
|
|
131
|
+
peerId: scope?.tokenId,
|
|
132
|
+
peerAcl,
|
|
133
|
+
getConfig: () => applyPeerAcl(getConfig(), peerAcl)
|
|
134
|
+
},
|
|
135
|
+
log
|
|
136
|
+
})
|
|
137
|
+
},
|
|
138
|
+
{ onerror: (error) => log?.error?.(`[dsh-mesh] handler error: ${error?.message || error}`) }
|
|
139
|
+
)
|
|
140
|
+
const nodeHandler = toNodeHandler(mcpHandler)
|
|
141
|
+
|
|
142
|
+
const requestListener = async (req, res) => {
|
|
143
|
+
let pathname = '/'
|
|
144
|
+
try {
|
|
145
|
+
pathname = new URL(req.url, 'http://mesh.local').pathname
|
|
146
|
+
} catch {
|
|
147
|
+
/* malformed URL falls through to 404 */
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (pathname === '/health') {
|
|
151
|
+
const liveConfig = getConfig()
|
|
152
|
+
const pairingState = readPairingState(meshHomeDir)
|
|
153
|
+
sendJson(res, 200, {
|
|
154
|
+
ok: true,
|
|
155
|
+
service: 'dsh-mesh',
|
|
156
|
+
version: MESH_VERSION,
|
|
157
|
+
node: liveConfig.node?.name || 'local',
|
|
158
|
+
/** Local DSH instances this node can bridge to (cluster addressing: <node>:<instance>). */
|
|
159
|
+
hosts: (liveConfig.hosts || []).map((entry) => ({
|
|
160
|
+
id: entry.id,
|
|
161
|
+
url: entry.url,
|
|
162
|
+
enabled: entry.enabled !== false
|
|
163
|
+
})),
|
|
164
|
+
uptimeSec: Math.round((Date.now() - Date.parse(NODE_STARTED_AT)) / 1000),
|
|
165
|
+
pid: process.pid,
|
|
166
|
+
startedAt: NODE_STARTED_AT,
|
|
167
|
+
tools: toolNames(ctx).length,
|
|
168
|
+
jobs: jobs.stats(),
|
|
169
|
+
tls: tlsCfg?.enabled === true,
|
|
170
|
+
tunnels: tunnelCache.get(),
|
|
171
|
+
pairing: {
|
|
172
|
+
enabled: liveConfig.pairing?.enabled === true,
|
|
173
|
+
codeActive: Boolean(pairingState && !pairingState.used && !pairingState.expired),
|
|
174
|
+
expiresAt: pairingState?.expiresAt ?? null
|
|
175
|
+
},
|
|
176
|
+
now: new Date().toISOString()
|
|
177
|
+
})
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Unauthenticated by design: a peer has no token yet, it redeems a one-time code instead.
|
|
182
|
+
if (pathname === '/pair') {
|
|
183
|
+
if (req.method !== 'POST') {
|
|
184
|
+
sendJson(res, 405, { code: 'E_INVALID_ARG', message: 'use POST /pair' }, { allow: 'POST' })
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
const liveConfig = getConfig()
|
|
188
|
+
if (liveConfig.pairing?.enabled !== true) {
|
|
189
|
+
sendJson(res, 403, { code: 'E_DENIED', message: 'pairing is disabled on this node' })
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
let body
|
|
193
|
+
try {
|
|
194
|
+
body = await readJsonBody(req)
|
|
195
|
+
} catch (error) {
|
|
196
|
+
sendJson(res, 400, { code: 'E_INVALID_ARG', message: error?.message || 'bad body' })
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
const claim = claimPairingCode({
|
|
200
|
+
home: meshHomeDir,
|
|
201
|
+
code: body?.code,
|
|
202
|
+
maxAttempts: Number(liveConfig.pairing?.maxAttempts) || 5
|
|
203
|
+
})
|
|
204
|
+
if (!claim.ok) {
|
|
205
|
+
// Small constant delay so the answer time does not reveal much; never say why.
|
|
206
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
207
|
+
await audit({ peer: 'pair', tool: 'pair', ok: false, code: claim.reason, args: ['code'] })
|
|
208
|
+
sendJson(
|
|
209
|
+
res,
|
|
210
|
+
401,
|
|
211
|
+
{
|
|
212
|
+
code: claim.reason === 'E_MAX_ATTEMPTS' ? 'E_MAX_ATTEMPTS' : 'E_AUTH',
|
|
213
|
+
message: 'pairing code rejected',
|
|
214
|
+
retryable: claim.reason !== 'E_MAX_ATTEMPTS' && claim.reason !== 'E_CODE_USED'
|
|
215
|
+
},
|
|
216
|
+
{ 'www-authenticate': 'Bearer realm="dsh-mesh"' }
|
|
217
|
+
)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
const baseId = sanitizePeerId(body?.peerName) || `peer-${Date.now().toString(36)}`
|
|
221
|
+
let created
|
|
222
|
+
try {
|
|
223
|
+
created = addInboundToken({
|
|
224
|
+
home: meshHomeDir,
|
|
225
|
+
id: baseId,
|
|
226
|
+
note: `paired ${new Date().toISOString()}`,
|
|
227
|
+
acl: body?.acl
|
|
228
|
+
})
|
|
229
|
+
} catch {
|
|
230
|
+
created = addInboundToken({
|
|
231
|
+
home: meshHomeDir,
|
|
232
|
+
id: `${baseId}-${Math.random().toString(36).slice(2, 6)}`,
|
|
233
|
+
note: `paired ${new Date().toISOString()}`,
|
|
234
|
+
acl: body?.acl
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
await audit({ peer: created.id, tool: 'pair', ok: true, args: ['code', 'peerName'] })
|
|
238
|
+
const scheme = tlsCfg?.enabled === true ? 'https' : 'http'
|
|
239
|
+
const address = httpServer.address()
|
|
240
|
+
const actualPort = typeof address === 'object' && address ? address.port : listenPort
|
|
241
|
+
sendJson(res, 200, {
|
|
242
|
+
ok: true,
|
|
243
|
+
token: created.token,
|
|
244
|
+
tokenId: created.id,
|
|
245
|
+
node: liveConfig.node?.name || 'local',
|
|
246
|
+
version: MESH_VERSION,
|
|
247
|
+
url: `${scheme}://${listenBind}:${actualPort}${mcpPath}`,
|
|
248
|
+
certFingerprint: tlsCfg?.enabled === true ? describeCert(meshHomeDir)?.fingerprint256 ?? null : null,
|
|
249
|
+
tools: toolNames(ctx).length
|
|
250
|
+
})
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (pathname !== mcpPath) {
|
|
255
|
+
sendJson(res, 404, { code: 'E_NOT_FOUND', message: `no route for ${pathname}`, mcpEndpoint: mcpPath })
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const secretsNow = getSecrets()
|
|
260
|
+
const tokenId = verifyBearer(req, secretsNow)
|
|
261
|
+
if (!tokenId) {
|
|
262
|
+
sendJson(
|
|
263
|
+
res,
|
|
264
|
+
401,
|
|
265
|
+
{ code: 'E_AUTH', message: 'missing or invalid bearer token' },
|
|
266
|
+
{ 'www-authenticate': 'Bearer realm="dsh-mesh"' }
|
|
267
|
+
)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
const peerAcl = peerAclForToken(secretsNow, tokenId)
|
|
271
|
+
|
|
272
|
+
calls += 1
|
|
273
|
+
const started = Date.now()
|
|
274
|
+
res.on('close', () => {
|
|
275
|
+
log?.debug?.(`[dsh-mesh] ${req.method} ${pathname} token=${tokenId} ${res.statusCode} ${Date.now() - started}ms`)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
await requestScope.run({ tokenId, peerAcl }, () => nodeHandler(req, res))
|
|
280
|
+
} catch (error) {
|
|
281
|
+
log?.error?.(`[dsh-mesh] transport failure: ${error?.message || error}`)
|
|
282
|
+
if (!res.headersSent) sendJson(res, 500, { code: 'E_INTERNAL', message: 'transport failure' })
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const httpServer =
|
|
287
|
+
tlsCfg?.enabled === true
|
|
288
|
+
? createHttpsServer(loadTlsMaterial({ home: meshHomeDir, cert: tlsCfg.cert, key: tlsCfg.key }), requestListener)
|
|
289
|
+
: createHttpServer(requestListener)
|
|
290
|
+
|
|
291
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
292
|
+
httpServer.once('error', rejectPromise)
|
|
293
|
+
httpServer.listen(listenPort, listenBind, () => {
|
|
294
|
+
httpServer.off('error', rejectPromise)
|
|
295
|
+
resolvePromise()
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
const address = httpServer.address()
|
|
300
|
+
const actualPort = typeof address === 'object' && address ? address.port : listenPort
|
|
301
|
+
const scheme = tlsCfg?.enabled === true ? 'https' : 'http'
|
|
302
|
+
const url = `${scheme}://${listenBind}:${actualPort}${mcpPath}`
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
port: actualPort,
|
|
306
|
+
url,
|
|
307
|
+
httpServer,
|
|
308
|
+
ctx,
|
|
309
|
+
auditPath: auditPath(meshHomeDir),
|
|
310
|
+
get requestCount() {
|
|
311
|
+
return calls
|
|
312
|
+
},
|
|
313
|
+
close() {
|
|
314
|
+
return new Promise((resolvePromise) => httpServer.close(() => resolvePromise()))
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|