@skillpie/dsh-remote 0.0.0-stage → 1.1.0
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/README.md +95 -2
- package/cordis.patch.yml +11 -0
- package/icon.svg +14 -0
- package/lib/bridge.js +377 -0
- package/lib/client.js +493 -0
- package/lib/console.js +434 -0
- package/lib/engine.js +367 -0
- package/lib/file-route.js +159 -0
- package/lib/index.js +496 -0
- package/lib/pipe.js +83 -0
- package/lib/protocol.js +122 -0
- package/lib/relay-core.js +1025 -0
- package/lib/remote.js +181 -0
- package/lib/ws-adapter.js +49 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +50 -4
package/lib/protocol.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-remote — shared wire constants and small helpers.
|
|
3
|
+
*
|
|
4
|
+
* Control channel (relay ↔ host, JSON text frames over one WebSocket):
|
|
5
|
+
* host → relay { t:'hello', v, hostId, code, token, name, dsh }
|
|
6
|
+
* { t:'hb' } heartbeat, every 15s
|
|
7
|
+
* { t:'res', id, ok, data?, error? } command receipts
|
|
8
|
+
* { t:'push', to, f } event push addressed to one visitor
|
|
9
|
+
* relay → host { t:'hello', v, ok, token?, error? } ack; token minted on first registration
|
|
10
|
+
* { t:'hb' } heartbeat echo
|
|
11
|
+
* { t:'cmd', id, from, cmd, p? } visitor command (from = visitor id)
|
|
12
|
+
*
|
|
13
|
+
* Visitor channel (relay ↔ browser, JSON text frames over /<code>/ws):
|
|
14
|
+
* relay → visitor { t:'welcome', v, visitor, host, sessions }
|
|
15
|
+
* { t:'res', id, ok, data?, error? }
|
|
16
|
+
* { t:'push', f }
|
|
17
|
+
* visitor → relay { t:'cmd', id, cmd, p? }
|
|
18
|
+
*
|
|
19
|
+
* Visitor command set lives in VISITOR_COMMANDS; 'visitor.hello' and
|
|
20
|
+
* 'visitor.gone' are relay-internal and never offered to the console.
|
|
21
|
+
*/
|
|
22
|
+
import { randomInt, randomUUID, createHash } from 'node:crypto'
|
|
23
|
+
|
|
24
|
+
export const PROTOCOL_VERSION = 1
|
|
25
|
+
|
|
26
|
+
/** Exact upgrade path where hosts dial the relay. */
|
|
27
|
+
export const HOST_UPGRADE_PATH = '/dsh-remote/host'
|
|
28
|
+
|
|
29
|
+
/** Owner panel JSON API, same-origin and behind the web GUI's session auth. */
|
|
30
|
+
export const OWNER_API_PATH = '/dsh-remote/api'
|
|
31
|
+
|
|
32
|
+
/** Commands the visitor console may issue. */
|
|
33
|
+
export const VISITOR_COMMANDS = new Set([
|
|
34
|
+
'host.info',
|
|
35
|
+
'sessions.list',
|
|
36
|
+
'sessions.state',
|
|
37
|
+
'sessions.create',
|
|
38
|
+
'sessions.send',
|
|
39
|
+
'sessions.cancel',
|
|
40
|
+
'sessions.observe',
|
|
41
|
+
'sessions.unobserve',
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
/** Relay-internal commands bridged to the engine, invisible to the console. */
|
|
45
|
+
export const INTERNAL_COMMANDS = new Set(['visitor.hello', 'visitor.gone'])
|
|
46
|
+
|
|
47
|
+
/** An 8-digit code: 10000000..99999999, no leading zero. */
|
|
48
|
+
export function generateCode() {
|
|
49
|
+
return String(randomInt(10_000_000, 100_000_000))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isValidCode(code) {
|
|
53
|
+
return typeof code === 'string' && /^[1-9]\d{7}$/.test(code)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function generateId() {
|
|
57
|
+
return randomUUID()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Relay-side only: hosts carry plaintext tokens, the registry stores hashes. */
|
|
61
|
+
export function hashToken(token) {
|
|
62
|
+
return createHash('sha256').update(String(token)).digest('hex')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Safe JSON frame parse with a hard byte cap. */
|
|
66
|
+
export function parseFrame(text, maxBytes = 64 * 1024) {
|
|
67
|
+
if (typeof text !== 'string' || text.length > maxBytes) return null
|
|
68
|
+
try {
|
|
69
|
+
const frame = JSON.parse(text)
|
|
70
|
+
return frame && typeof frame === 'object' ? frame : null
|
|
71
|
+
} catch {
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function nowSeconds() {
|
|
77
|
+
return Math.floor(Date.now() / 1000)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* ── GUI splice mode ────────────────────────────────────────────────────
|
|
81
|
+
*
|
|
82
|
+
* In `gui` mode the relay transparently mounts the host's real web GUI
|
|
83
|
+
* under `/<code>/`: HTTP requests and the GUI's single mux WebSocket
|
|
84
|
+
* (`/api/remote.mux`) are bridged frame-by-frame, the host side serving
|
|
85
|
+
* them from its own web server over loopback with an automatically minted
|
|
86
|
+
* owner session cookie.
|
|
87
|
+
*
|
|
88
|
+
* Frames (JSON text unless noted):
|
|
89
|
+
* relay → host { t:'hreq', id, m, p, h } begin HTTP proxy request
|
|
90
|
+
* { t:'hend', id } request body complete
|
|
91
|
+
* { t:'wopen', id, p, h } open spliced WebSocket
|
|
92
|
+
* { t:'wclose', id, code, reason }
|
|
93
|
+
* host → relay { t:'hres', id, s, h, sc } status, headers, set-cookie[]
|
|
94
|
+
* { t:'hfail', id, error }
|
|
95
|
+
* { t:'wok', id } / { t:'wfail', id, error }
|
|
96
|
+
* { t:'wclose', id, code, reason }
|
|
97
|
+
* both { t:'hchunk', id } (binary) request/response body chunk
|
|
98
|
+
* { t:'wdata', id } (binary) spliced socket bytes
|
|
99
|
+
*
|
|
100
|
+
* Binary envelope: `JSON(meta) 0x00 payload` — JSON never contains a raw
|
|
101
|
+
* NUL, so the first 0x00 byte separates header from payload.
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
export function encodeBin(meta, payload) {
|
|
105
|
+
const header = Buffer.from(JSON.stringify(meta), 'utf8')
|
|
106
|
+
const body = payload === undefined ? EMPTY : Buffer.isBuffer(payload) ? payload : Buffer.from(payload)
|
|
107
|
+
return Buffer.concat([header, SEPARATOR, body])
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function decodeBin(buffer) {
|
|
111
|
+
const at = buffer.indexOf(0)
|
|
112
|
+
if (at === -1) return undefined
|
|
113
|
+
try {
|
|
114
|
+
const meta = JSON.parse(buffer.slice(0, at).toString('utf8'))
|
|
115
|
+
return meta && typeof meta === 'object' ? { meta, payload: buffer.slice(at + 1) } : undefined
|
|
116
|
+
} catch {
|
|
117
|
+
return undefined
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const SEPARATOR = Buffer.from([0])
|
|
122
|
+
const EMPTY = Buffer.alloc(0)
|