dsh-mesh-node 0.0.0-stage → 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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -2
  3. package/package.json +43 -4
  4. package/src/cli.js +1246 -0
  5. package/src/core/acl.js +81 -0
  6. package/src/core/audit.js +75 -0
  7. package/src/core/autostart.js +119 -0
  8. package/src/core/cluster.js +341 -0
  9. package/src/core/config.js +622 -0
  10. package/src/core/desktop-proxy.js +85 -0
  11. package/src/core/ds-web.js +394 -0
  12. package/src/core/host-agent.js +263 -0
  13. package/src/core/jobs.js +259 -0
  14. package/src/core/pairing.js +141 -0
  15. package/src/core/peer-client.js +273 -0
  16. package/src/core/peer-doctor.js +294 -0
  17. package/src/core/peer-http.js +61 -0
  18. package/src/core/repair.js +93 -0
  19. package/src/core/restart-policy.js +21 -0
  20. package/src/core/result.js +29 -0
  21. package/src/core/sakura-api.js +103 -0
  22. package/src/core/session-profile.js +146 -0
  23. package/src/core/tls.js +160 -0
  24. package/src/core/tunnels.js +212 -0
  25. package/src/core/util.js +68 -0
  26. package/src/e2e-autostart.js +58 -0
  27. package/src/e2e-channel.js +384 -0
  28. package/src/e2e-cluster.js +417 -0
  29. package/src/e2e-host.js +411 -0
  30. package/src/e2e-onboard.js +232 -0
  31. package/src/e2e-pair.js +317 -0
  32. package/src/e2e-plugin.js +395 -0
  33. package/src/e2e-repair.js +297 -0
  34. package/src/index.js +20 -0
  35. package/src/peer-bridge.js +124 -0
  36. package/src/selftest.js +872 -0
  37. package/src/server.js +317 -0
  38. package/src/tools/alerts.js +80 -0
  39. package/src/tools/channel.js +253 -0
  40. package/src/tools/cluster.js +235 -0
  41. package/src/tools/config-patch.js +182 -0
  42. package/src/tools/control-apps.js +469 -0
  43. package/src/tools/control-path.js +160 -0
  44. package/src/tools/control.js +232 -0
  45. package/src/tools/core.js +79 -0
  46. package/src/tools/delegate.js +653 -0
  47. package/src/tools/fs.js +436 -0
  48. package/src/tools/git.js +266 -0
  49. package/src/tools/host.js +305 -0
  50. package/src/tools/index.js +122 -0
  51. package/src/tools/repair-fs.js +130 -0
  52. package/src/tools/repair.js +317 -0
  53. package/src/tools/session-history.js +60 -0
  54. package/src/tools/shell.js +66 -0
  55. package/src/tools/tasks.js +117 -0
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * e2e for pairing (one-time code -> per-peer token) and per-token ACL.
4
+ *
5
+ * Everything runs against a real dsh-mesh server on 127.0.0.1 with a throwaway
6
+ * DSH_MESH_HOME, so no real peer or model is involved. Bad samples are first-class:
7
+ * wrong code, reused code, exhausted attempts, expired code, disabled pairing,
8
+ * narrowed ACL, revoked token.
9
+ *
10
+ * Usage: node src/e2e-pair.js
11
+ */
12
+ import { mkdtempSync, rmSync, writeFileSync, readFileSync, statSync, readdirSync } from 'node:fs'
13
+ import { tmpdir } from 'node:os'
14
+ import { join } from 'node:path'
15
+ import { Client } from '@modelcontextprotocol/client'
16
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/client'
17
+ import { startMeshServer } from './server.js'
18
+ import {
19
+ defaultConfig,
20
+ addInboundToken,
21
+ removeInboundToken,
22
+ loadSecrets,
23
+ pairingPath,
24
+ saveConfig
25
+ } from './core/config.js'
26
+ import { createPairingCode, readPairingState } from './core/pairing.js'
27
+ import { generateCert, describeCert } from './core/tls.js'
28
+ import { postJson } from './core/peer-http.js'
29
+
30
+ const quiet = { debug() {}, info() {}, warn() {}, error() {} }
31
+ const POSIX = process.platform !== 'win32'
32
+ const results = []
33
+ function assert(cond, message) {
34
+ if (!cond) throw new Error(message)
35
+ }
36
+ async function check(name, fn) {
37
+ try {
38
+ await fn()
39
+ results.push({ name, ok: true })
40
+ console.log(`PASS ${name}`)
41
+ } catch (error) {
42
+ results.push({ name, ok: false, error: error?.message || String(error) })
43
+ console.log(`FAIL ${name} — ${error?.message || error}`)
44
+ }
45
+ }
46
+
47
+ function freshConfig() {
48
+ const config = defaultConfig()
49
+ config.node.name = 'pair-test'
50
+ config.node.port = 0
51
+ config.node.tls = { enabled: false, cert: '', key: '' }
52
+ config.pairing = { enabled: true, codeTtlSec: 300, maxAttempts: 5 }
53
+ config.acl.fsRoots = [tmpdir()]
54
+ return config
55
+ }
56
+
57
+ async function pairRequest(port, body) {
58
+ const res = await fetch(`http://127.0.0.1:${port}/pair`, {
59
+ method: 'POST',
60
+ headers: { 'content-type': 'application/json' },
61
+ body: JSON.stringify(body)
62
+ })
63
+ let parsed
64
+ try {
65
+ parsed = await res.json()
66
+ } catch {
67
+ parsed = {}
68
+ }
69
+ return { status: res.status, body: parsed }
70
+ }
71
+
72
+ async function connectMesh(port, token) {
73
+ const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${port}/mcp`), {
74
+ requestInit: { headers: { Authorization: `Bearer ${token}` } }
75
+ })
76
+ const client = new Client({ name: 'dsh-mesh-pair-e2e', version: '0.0.1' })
77
+ await client.connect(transport)
78
+ return client
79
+ }
80
+
81
+ function parsedToolResult(result) {
82
+ const text = result?.content?.find?.((c) => c.type === 'text')?.text
83
+ return JSON.parse(text)
84
+ }
85
+
86
+ export async function runE2ePair() {
87
+ const home = mkdtempSync(join(tmpdir(), 'dsh-mesh-pair-'))
88
+ const tmpPeerB = mkdtempSync(join(tmpdir(), 'dsh-mesh-pair-tls-'))
89
+ const config = freshConfig()
90
+ saveConfig(config, home)
91
+ let srv
92
+ let srv2
93
+ const clients = []
94
+
95
+ try {
96
+ srv = await startMeshServer({ home, log: quiet, port: 0 })
97
+ const port = srv.port
98
+
99
+ await check('health advertises pairing without exposing a code (bad sample: no code leak)', async () => {
100
+ const res = await fetch(`http://127.0.0.1:${port}/health`)
101
+ const body = await res.json()
102
+ assert(body.pairing?.enabled === true, 'pairing.enabled missing from /health')
103
+ assert(body.pairing?.codeActive === false, 'a code should not be active yet')
104
+ assert(!JSON.stringify(body).match(/[A-Z0-9]{4}-[A-Z0-9]{4}/), 'health leaked something code-shaped')
105
+ })
106
+
107
+ await check('pairing code has the human-friendly shape XXXX-XXXX', async () => {
108
+ const created = createPairingCode({ home, ttlSec: 300 })
109
+ assert(/^[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(created.code), `unexpected code ${created.code}`)
110
+ assert(new Date(created.expiresAt).getTime() > Date.now(), 'code already expired')
111
+ globalThis.__pairCode = created.code
112
+ })
113
+
114
+ await check('wrong pairing code is rejected without saying why (bad sample)', async () => {
115
+ const res = await pairRequest(port, { code: 'ZZZZ-ZZZZ', peerName: 'intruder' })
116
+ assert(res.status === 401, `expected 401, got ${res.status}`)
117
+ assert(res.body.code === 'E_AUTH', `expected E_AUTH, got ${res.body.code}`)
118
+ assert(!('token' in res.body), 'a token leaked on a failed pairing')
119
+ })
120
+
121
+ await check('the right code is redeemed for a per-peer token', async () => {
122
+ const res = await pairRequest(port, { code: globalThis.__pairCode, peerName: 'company' })
123
+ assert(res.status === 200, `expected 200, got ${res.status} ${JSON.stringify(res.body)}`)
124
+ assert(res.body.ok === true, 'ok flag missing')
125
+ assert(typeof res.body.token === 'string' && res.body.token.length === 64, 'token is not 32 bytes hex')
126
+ assert(res.body.tokenId === 'company', `unexpected tokenId ${res.body.tokenId}`)
127
+ assert(res.body.url.endsWith('/mcp'), `unexpected url ${res.body.url}`)
128
+ assert(res.body.certFingerprint === null, 'plain http must not report a fingerprint')
129
+ globalThis.__pairedToken = res.body.token
130
+ })
131
+
132
+ await check('the paired token really connects over MCP', async () => {
133
+ const client = await connectMesh(port, globalThis.__pairedToken)
134
+ clients.push(client)
135
+ const listed = await client.listTools()
136
+ assert(Array.isArray(listed.tools) && listed.tools.length >= 20, `only ${listed.tools?.length} tools listed`)
137
+ globalThis.__pairedClient = client
138
+ })
139
+
140
+ await check('the same code cannot be redeemed twice (bad sample)', async () => {
141
+ const res = await pairRequest(port, { code: globalThis.__pairCode, peerName: 'copycat' })
142
+ assert(res.status === 401, `expected 401 on replay, got ${res.status}`)
143
+ assert(res.body.code === 'E_AUTH', `expected E_AUTH, got ${res.body.code}`)
144
+ })
145
+
146
+ await check('missing code is rejected (bad sample)', async () => {
147
+ const res = await pairRequest(port, { peerName: 'nocode' })
148
+ assert(res.status === 401, `expected 401, got ${res.status}`)
149
+ })
150
+
151
+ await check('guessing burns the code after maxAttempts (bad sample)', async () => {
152
+ const created = createPairingCode({ home, ttlSec: 300 })
153
+ let lastStatus = 0
154
+ let lastCode = ''
155
+ for (let i = 0; i < 5; i += 1) {
156
+ const bad = await pairRequest(port, { code: 'AAAA-AAAA', peerName: 'brute' })
157
+ lastStatus = bad.status
158
+ lastCode = bad.body.code
159
+ }
160
+ assert(lastStatus === 401, `expected 401 after guesses, got ${lastStatus}`)
161
+ assert(lastCode === 'E_MAX_ATTEMPTS', `expected E_MAX_ATTEMPTS, got ${lastCode}`)
162
+ const correct = await pairRequest(port, { code: created.code, peerName: 'brute' })
163
+ assert(correct.status === 401, 'a burnt code was still accepted')
164
+ })
165
+
166
+ await check('an expired code is rejected (bad sample)', async () => {
167
+ const created = createPairingCode({ home, ttlSec: 300 })
168
+ const file = pairingPath(home)
169
+ const record = JSON.parse(readFileSync(file, 'utf8'))
170
+ record.expiresAt = new Date(Date.now() - 1000).toISOString()
171
+ writeFileSync(file, JSON.stringify(record, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 })
172
+ const res = await pairRequest(port, { code: created.code, peerName: 'late' })
173
+ assert(res.status === 401, `expected 401 on expired code, got ${res.status}`)
174
+ })
175
+
176
+ await check('pairing.json stores only a hash, never the code (bad sample: leak)', async () => {
177
+ const created = createPairingCode({ home, ttlSec: 300 })
178
+ const text = readFileSync(pairingPath(home), 'utf8')
179
+ assert(!text.includes(created.code.replace('-', '')), 'the raw code is on disk')
180
+ assert(!text.includes(created.code), 'the formatted code is on disk')
181
+ // Node on Windows reports 0o666 for every file: chmod is advisory there, so the
182
+ // mode assertion only means something on POSIX.
183
+ if (POSIX) {
184
+ const mode = statSync(pairingPath(home)).mode & 0o777
185
+ assert((mode & 0o077) === 0, `pairing.json is too permissive: ${mode.toString(8)}`)
186
+ }
187
+ })
188
+
189
+ await check('pairing can be switched off (bad sample: disabled node)', async () => {
190
+ const offConfig = { ...config, pairing: { ...config.pairing, enabled: false } }
191
+ saveConfig(offConfig, home)
192
+ await new Promise((r) => setTimeout(r, 30)) // let the mtime-based reload notice
193
+ const created = createPairingCode({ home, ttlSec: 300 })
194
+ const res = await pairRequest(port, { code: created.code, peerName: 'nope' })
195
+ assert(res.status === 403, `expected 403 when pairing is off, got ${res.status}`)
196
+ saveConfig(config, home)
197
+ await new Promise((r) => setTimeout(r, 30))
198
+ })
199
+
200
+ await check('per-token ACL narrows what a peer can call', async () => {
201
+ const created = addInboundToken({ home, id: 'narrow', note: 'e2e', acl: { allow: ['mesh_self'] } })
202
+ const client = await connectMesh(port, created.token)
203
+ clients.push(client)
204
+ const denied = await client.callTool({ name: 'fs_read', arguments: { path: join(home, 'config.json') } })
205
+ const deniedBody = parsedToolResult(denied)
206
+ assert(denied.isError === true, 'fs_read should have been denied for the narrowed token')
207
+ assert(deniedBody.code === 'E_DENIED', `expected E_DENIED, got ${deniedBody.code}`)
208
+ const me = parsedToolResult(await client.callTool({ name: 'mesh_self', arguments: {} }))
209
+ assert(me.acl.you.tokenId === 'narrow', `mesh_self reported token ${me.acl.you.tokenId}`)
210
+ assert(me.acl.allow.length === 1 && me.acl.allow[0] === 'mesh_self', `narrowed allow leaked: ${me.acl.allow}`)
211
+ })
212
+
213
+ await check('a peer-specific deny wins even when the node allows the tool', async () => {
214
+ const created = addInboundToken({ home, id: 'denied-peer', note: 'e2e', acl: { deny: ['git_status'] } })
215
+ const client = await connectMesh(port, created.token)
216
+ clients.push(client)
217
+ const body = parsedToolResult(await client.callTool({ name: 'git_status', arguments: { path: home } }))
218
+ assert(body.code === 'E_DENIED', `expected E_DENIED, got ${body.code}`)
219
+ })
220
+
221
+ await check('revoking a token cuts access immediately (bad sample)', async () => {
222
+ const created = addInboundToken({ home, id: 'temp', note: 'e2e' })
223
+ const client = await connectMesh(port, created.token)
224
+ clients.push(client)
225
+ await client.listTools() // works before
226
+ removeInboundToken({ home, id: 'temp' })
227
+ let failed = false
228
+ try {
229
+ await client.listTools()
230
+ } catch {
231
+ failed = true
232
+ }
233
+ assert(failed, 'a revoked token still worked')
234
+ })
235
+
236
+ await check('TLS pairing: pinned fingerprint accepts a self-signed peer', async () => {
237
+ const config2 = defaultConfig()
238
+ config2.node.name = 'pair-tls'
239
+ config2.node.port = 0
240
+ config2.node.tls = { enabled: true, cert: '', key: '' }
241
+ config2.pairing = { enabled: true, codeTtlSec: 300, maxAttempts: 5 }
242
+ config2.acl.fsRoots = [tmpdir()]
243
+ await generateCert({ home: tmpPeerB, sans: ['127.0.0.1', 'localhost'], force: true })
244
+ saveConfig(config2, tmpPeerB)
245
+ srv2 = await startMeshServer({ home: tmpPeerB, log: quiet, port: 0 })
246
+ const fingerprint = describeCert(tmpPeerB).fingerprint256
247
+ const created = createPairingCode({ home: tmpPeerB, ttlSec: 120 })
248
+ const res = await postJson(
249
+ `https://127.0.0.1:${srv2.port}/pair`,
250
+ { code: created.code, peerName: 'pinned' },
251
+ { fingerprint }
252
+ )
253
+ assert(res.status === 200 && res.body.ok === true, `pinned pairing failed: ${res.status} ${JSON.stringify(res.body)}`)
254
+ assert(
255
+ res.fingerprint && res.fingerprint.replace(/:/g, '') === fingerprint.replace(/:/g, ''),
256
+ 'the socket fingerprint does not match describeCert()'
257
+ )
258
+ })
259
+
260
+ await check('TLS pairing: a wrong pin aborts before the code is used (bad sample)', async () => {
261
+ const created = createPairingCode({ home: tmpPeerB, ttlSec: 120 })
262
+ let code = ''
263
+ try {
264
+ await postJson(
265
+ `https://127.0.0.1:${srv2.port}/pair`,
266
+ { code: created.code, peerName: 'mitm' },
267
+ { fingerprint: '11'.repeat(32) }
268
+ )
269
+ } catch (error) {
270
+ code = error?.code || error?.message || ''
271
+ }
272
+ assert(code === 'E_PIN_MISMATCH', `expected E_PIN_MISMATCH, got ${code}`)
273
+ const state = readPairingState(tmpPeerB)
274
+ assert(state && state.used === false && state.attempts === 0, 'the code was consumed even though the pin failed')
275
+ })
276
+
277
+ await check('advertising the peer token does not leak into audit values', async () => { const auditText = readFileSync(join(home, 'audit.jsonl'), 'utf8')
278
+ assert(auditText.includes('"tool":"pair"'), 'pairing was not audited')
279
+ assert(!auditText.includes(globalThis.__pairedToken), 'the token was written into the audit log')
280
+ })
281
+
282
+ await check('secrets.json holds the paired token with 0600', async () => {
283
+ const secrets = loadSecrets(home)
284
+ assert(secrets.inbound.some((e) => e.id === 'company'), 'paired token missing from secrets.json')
285
+ if (POSIX) {
286
+ const mode = statSync(join(home, 'secrets.json')).mode & 0o777
287
+ assert((mode & 0o077) === 0, `secrets.json is too permissive: ${mode.toString(8)}`)
288
+ }
289
+ const files = readdirSync(home)
290
+ assert(files.includes('pairing.json'), 'pairing.json missing')
291
+ })
292
+ } finally {
293
+ for (const client of clients) {
294
+ try {
295
+ await client.close?.()
296
+ } catch {
297
+ /* ignore */
298
+ }
299
+ }
300
+ if (srv) await srv.close()
301
+ if (srv2) await srv2.close()
302
+ rmSync(home, { recursive: true, force: true })
303
+ rmSync(tmpPeerB, { recursive: true, force: true })
304
+ }
305
+
306
+ const failed = results.filter((r) => !r.ok)
307
+ console.log(`\ne2e-pair: ${results.length - failed.length}/${results.length} passed`)
308
+ if (failed.length > 0) {
309
+ for (const f of failed) console.log(` FAILED: ${f.name} — ${f.error}`)
310
+ return 1
311
+ }
312
+ return 0
313
+ }
314
+
315
+ if (process.argv[1]?.endsWith('e2e-pair.js')) {
316
+ runE2ePair().then((code) => process.exit(code))
317
+ }