hsync 0.30.1 → 0.31.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/.github/workflows/ci.yml +32 -0
- package/.husky/pre-commit +1 -0
- package/.prettierrc +7 -0
- package/Readme.md +1 -0
- package/cli.js +103 -56
- package/config.js +6 -6
- package/connection.js +44 -48
- package/dist/hsync.js +1082 -831
- package/dist/hsync.min.js +28862 -1
- package/dist/hsync.min.js.LICENSE.txt +11 -1
- package/eslint.config.js +26 -0
- package/hsync-web.js +18 -17
- package/hsync.js +14 -18
- package/index.js +3 -3
- package/lib/fetch.js +2 -4
- package/lib/peers.js +69 -68
- package/lib/rtc-node.js +35 -34
- package/lib/rtc-web.js +60 -58
- package/lib/socket-listeners.js +16 -22
- package/lib/socket-map.js +5 -5
- package/lib/socket-relays.js +12 -17
- package/lib/web-handler.js +8 -14
- package/package.json +61 -18
- package/shell.js +4 -8
- package/test/mocks/mqtt.mock.js +25 -0
- package/test/mocks/net.mock.js +34 -0
- package/test/mocks/rtc.mock.js +9 -0
- package/test/setup.js +10 -0
- package/test/unit/config.test.js +36 -0
- package/test/unit/fetch.test.js +189 -0
- package/test/unit/peers.test.js +275 -0
- package/test/unit/socket-listeners.test.js +319 -0
- package/test/unit/socket-map.test.js +64 -0
- package/test/unit/socket-relays.test.js +315 -0
- package/test/unit/web-handler.test.js +223 -0
- package/todo.md +324 -0
- package/vitest.config.js +15 -0
- package/webpack.config.js +24 -8
package/todo.md
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# hsync Client - Auth Feature TODO
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Add HTTP proxy authentication so services without built-in auth (like LM Studio) can be protected when exposed via hsync.
|
|
6
|
+
|
|
7
|
+
## Design
|
|
8
|
+
|
|
9
|
+
**Simple case:** One `--token` flag enables auth
|
|
10
|
+
```bash
|
|
11
|
+
hsync -p 1234 --token "my-secret"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**Accepts both:**
|
|
15
|
+
- `Authorization: Bearer my-secret`
|
|
16
|
+
- `Authorization: Basic base64(hsync:my-secret)`
|
|
17
|
+
- URL embedded: `https://hsync:my-secret@xyz.hsync.tech/...`
|
|
18
|
+
|
|
19
|
+
**Optional username override:**
|
|
20
|
+
```bash
|
|
21
|
+
hsync -p 1234 --token "my-secret" --username "admin"
|
|
22
|
+
# Basic auth expects admin:my-secret
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Programmatic:**
|
|
26
|
+
```javascript
|
|
27
|
+
const con = await hsync.createConnection({
|
|
28
|
+
port: 1234,
|
|
29
|
+
token: 'my-secret',
|
|
30
|
+
username: 'admin' // optional, defaults to 'hsync'
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Implementation Tasks
|
|
35
|
+
|
|
36
|
+
### 1. CLI Changes (cli.js)
|
|
37
|
+
|
|
38
|
+
- [ ] Add `--token <string>` option
|
|
39
|
+
- [ ] Add `--username <string>` option (optional, default: 'hsync')
|
|
40
|
+
- [ ] Pass token/username to connection config
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
.addOption(new Option('-t, --token <string>', 'auth token for HTTP proxy').env('HSYNC_TOKEN'))
|
|
44
|
+
.addOption(new Option('-u, --username <string>', 'username for Basic auth').env('HSYNC_USERNAME').default('hsync'))
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Connection Changes (connection.js)
|
|
48
|
+
|
|
49
|
+
- [ ] Accept `token` and `username` in config
|
|
50
|
+
- [ ] After MQTT connect, call `serverPeer.methods.setHttpAuth()` to register auth with server
|
|
51
|
+
- [ ] Store auth config on hsyncClient object
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
if (config.token) {
|
|
55
|
+
await hsyncClient.serverPeer.methods.setHttpAuth({
|
|
56
|
+
token: config.token,
|
|
57
|
+
username: config.username || 'hsync'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3. Config Changes (config.js)
|
|
63
|
+
|
|
64
|
+
- [ ] Add `token` and `username` to baseConfig from env vars
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
token: env.HSYNC_TOKEN,
|
|
68
|
+
username: env.HSYNC_USERNAME || 'hsync',
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Server Dependency
|
|
72
|
+
|
|
73
|
+
This feature requires hsync-server to implement `setHttpAuth` RPC method and auth checking. See hsync-server/todo.md.
|
|
74
|
+
|
|
75
|
+
## Testing
|
|
76
|
+
|
|
77
|
+
- [ ] Test Bearer auth: `curl -H "Authorization: Bearer my-secret" ...`
|
|
78
|
+
- [ ] Test Basic auth: `curl -u hsync:my-secret ...`
|
|
79
|
+
- [ ] Test URL embedded: `curl https://hsync:my-secret@... `
|
|
80
|
+
- [ ] Test wrong token returns 401
|
|
81
|
+
- [ ] Test no token set = no auth checking (open proxy)
|
|
82
|
+
- [ ] Test custom username with Basic auth
|
|
83
|
+
|
|
84
|
+
## Documentation
|
|
85
|
+
|
|
86
|
+
- [ ] Update README with auth examples
|
|
87
|
+
- [ ] Add examples for LM Studio use case
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
# Sub-subdomain Routing Feature TODO
|
|
92
|
+
|
|
93
|
+
## Overview
|
|
94
|
+
|
|
95
|
+
Route multiple local services through a single hsync connection. One client, multiple services - no need for separate connections per service.
|
|
96
|
+
|
|
97
|
+
Less overhead: one WebSocket connection instead of many, and one Node process (CLI) instead of multiple instances.
|
|
98
|
+
|
|
99
|
+
Uses a configurable separator (e.g., `_`) to stay within one subdomain level for wildcard cert compatibility.
|
|
100
|
+
|
|
101
|
+
## Why Underscore?
|
|
102
|
+
|
|
103
|
+
Wildcard certs (`*.myhsyncserver.com`) only cover ONE subdomain level:
|
|
104
|
+
```
|
|
105
|
+
✅ myapp.myhsyncserver.com - covered
|
|
106
|
+
✅ api.myhsyncserver.com - covered
|
|
107
|
+
❌ api.myapp.myhsyncserver.com - NOT covered (two levels)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Using underscore keeps it to one level:
|
|
111
|
+
```
|
|
112
|
+
✅ api_myapp.myhsyncserver.com - covered by *.myhsyncserver.com
|
|
113
|
+
✅ frontend_myapp.myhsyncserver.com - covered
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
One wildcard cert, unlimited sub-services per client.
|
|
117
|
+
|
|
118
|
+
## Design
|
|
119
|
+
|
|
120
|
+
**Example:**
|
|
121
|
+
```
|
|
122
|
+
api_myapp.myhsyncserver.com → hsync client "myapp" → localhost:3001
|
|
123
|
+
frontend_myapp.myhsyncserver.com → hsync client "myapp" → localhost:3000
|
|
124
|
+
myapp.myhsyncserver.com → hsync client "myapp" → localhost:3000 (default)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**CLI:**
|
|
128
|
+
```bash
|
|
129
|
+
hsync -p 3000 --subsub api:3001 --subsub frontend:3002
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Programmatic:**
|
|
133
|
+
```javascript
|
|
134
|
+
const con = await hsync.createConnection({
|
|
135
|
+
port: 3000, // default
|
|
136
|
+
subsubs: {
|
|
137
|
+
'api': 3001,
|
|
138
|
+
'frontend': 3002
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Topic Structure
|
|
144
|
+
|
|
145
|
+
New `websub/` namespace to avoid clashing with existing `web/` close action:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
web/{hostname}/{socketId} ← no prefix (existing)
|
|
149
|
+
web/{hostname}/{socketId}/close ← close (existing)
|
|
150
|
+
|
|
151
|
+
websub/{hostname}/{prefix}/{socketId} ← with prefix (new)
|
|
152
|
+
websub/{hostname}/{prefix}/{socketId}/close ← close with prefix (new)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Raw binary payloads throughout - no encoding overhead.
|
|
156
|
+
|
|
157
|
+
## Implementation Tasks
|
|
158
|
+
|
|
159
|
+
### 1. CLI Changes (cli.js)
|
|
160
|
+
|
|
161
|
+
- [ ] Add `--subsub <prefix:port>` option (repeatable)
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
.addOption(new Option('--subsub <mapping>', 'sub-subdomain prefix to port mapping (e.g., api:3001)').argParser(collectSubsubs))
|
|
165
|
+
|
|
166
|
+
function collectSubsubs(value, previous) {
|
|
167
|
+
const [prefix, port] = value.split(':');
|
|
168
|
+
return { ...previous, [prefix]: parseInt(port, 10) };
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 2. Connection Changes (connection.js)
|
|
173
|
+
|
|
174
|
+
- [ ] Accept `subsubs` in config
|
|
175
|
+
- [ ] Subscribe to `websub/{hostname}/#` if subsubs configured
|
|
176
|
+
- [ ] Handle `websub/` messages in message handler
|
|
177
|
+
|
|
178
|
+
```javascript
|
|
179
|
+
if (config.subsubs) {
|
|
180
|
+
mqConn.subscribe(`websub/${myHostName}/#`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
mqConn.on('message', (topic, message) => {
|
|
184
|
+
const [name, hostName, ...rest] = topic.split('/');
|
|
185
|
+
|
|
186
|
+
if (name === 'websub') {
|
|
187
|
+
const [prefix, socketId, action] = rest;
|
|
188
|
+
const port = config.subsubs[prefix] || config.port;
|
|
189
|
+
webHandler.handleWebRequest(hostName, socketId, action, message, port);
|
|
190
|
+
}
|
|
191
|
+
// ... existing web/ handling
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 3. Web Handler Changes (lib/web-handler.js)
|
|
196
|
+
|
|
197
|
+
- [ ] Accept optional port parameter
|
|
198
|
+
- [ ] Use passed port instead of default config.port
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
function handleWebRequest(hostName, socketId, action, message, port = defaultPort) {
|
|
202
|
+
// ... use port for socket.connect()
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 4. Config Changes (config.js)
|
|
207
|
+
|
|
208
|
+
- [ ] Add subsubs to config structure
|
|
209
|
+
|
|
210
|
+
## Server Dependency
|
|
211
|
+
|
|
212
|
+
Requires hsync-server to:
|
|
213
|
+
- Have `HSYNC_SUBSUB_SEPARATOR` configured (e.g., `_`) - feature disabled if not set
|
|
214
|
+
- Parse sub-subdomain prefix (e.g., `api_myapp` → prefix: `api`, base: `myapp`)
|
|
215
|
+
- Route to client based on base hostname
|
|
216
|
+
- Publish to `websub/{base}/{prefix}/{socketId}` when prefix present
|
|
217
|
+
|
|
218
|
+
See hsync-server/todo.md.
|
|
219
|
+
|
|
220
|
+
## Testing
|
|
221
|
+
|
|
222
|
+
- [ ] Test `api_myapp.server.com` forwards to port 3001
|
|
223
|
+
- [ ] Test `frontend_myapp.server.com` forwards to port 3000
|
|
224
|
+
- [ ] Test `myapp.server.com` (no prefix) forwards to default port
|
|
225
|
+
- [ ] Test unknown prefix falls back to default port
|
|
226
|
+
- [ ] Test close action works with prefix
|
|
227
|
+
- [ ] Test existing non-prefixed routing still works
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
# Testing & Linting TODO
|
|
232
|
+
|
|
233
|
+
## Overview
|
|
234
|
+
|
|
235
|
+
Add proper test coverage and linting to ensure code quality and catch regressions.
|
|
236
|
+
|
|
237
|
+
## Testing
|
|
238
|
+
|
|
239
|
+
### Setup
|
|
240
|
+
|
|
241
|
+
- [x] Add test framework (vitest or jest) - **DONE**
|
|
242
|
+
- [x] Add test script to package.json - **DONE**
|
|
243
|
+
- [x] Set up test directory structure - **DONE**
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
hsync/
|
|
247
|
+
test/
|
|
248
|
+
unit/
|
|
249
|
+
config.test.js
|
|
250
|
+
web-handler.test.js
|
|
251
|
+
peers.test.js
|
|
252
|
+
socket-listeners.test.js
|
|
253
|
+
socket-relays.test.js
|
|
254
|
+
integration/
|
|
255
|
+
connection.test.js
|
|
256
|
+
auth.test.js
|
|
257
|
+
subsub.test.js
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Unit Tests
|
|
261
|
+
|
|
262
|
+
- [x] **config.js** - env var parsing, multi-server config - **DONE (3 tests)**
|
|
263
|
+
- [x] **web-handler.js** - request handling, socket management - **DONE (15 tests)**
|
|
264
|
+
- [x] **peers.js** - RPC peer creation, message handling - **DONE (27 tests)**
|
|
265
|
+
- [x] **socket-listeners.js** - listener creation, data forwarding - **DONE (19 tests)**
|
|
266
|
+
- [x] **socket-relays.js** - relay creation, connection handling - **DONE (19 tests)**
|
|
267
|
+
- [x] **socket-map.js** - packet handling - **DONE (5 tests)**
|
|
268
|
+
- [x] **fetch.js** - fetch wrapper - **DONE (11 tests)**
|
|
269
|
+
|
|
270
|
+
**Total: 99 tests passing**
|
|
271
|
+
|
|
272
|
+
### Integration Tests
|
|
273
|
+
|
|
274
|
+
- [ ] Connection to hsync-server (mock or real)
|
|
275
|
+
- [ ] Web request forwarding end-to-end
|
|
276
|
+
- [ ] Auth token validation
|
|
277
|
+
- [ ] Sub-subdomain routing
|
|
278
|
+
- [ ] WebRTC peer connection (if possible to test)
|
|
279
|
+
|
|
280
|
+
### Test Utilities
|
|
281
|
+
|
|
282
|
+
- [x] Mock MQTT connection - **DONE**
|
|
283
|
+
- [x] Mock net.Socket - **DONE**
|
|
284
|
+
- [ ] Test fixtures for HTTP requests/responses
|
|
285
|
+
|
|
286
|
+
## Linting
|
|
287
|
+
|
|
288
|
+
### Setup
|
|
289
|
+
|
|
290
|
+
- [x] Add/update ESLint config (eslint.config.js - flat config) - **DONE**
|
|
291
|
+
- [x] Add lint script to package.json - **DONE**
|
|
292
|
+
- [x] Add pre-commit hook (husky + lint-staged) - **DONE**
|
|
293
|
+
|
|
294
|
+
```json
|
|
295
|
+
{
|
|
296
|
+
"scripts": {
|
|
297
|
+
"lint": "eslint .",
|
|
298
|
+
"lint:fix": "eslint . --fix",
|
|
299
|
+
"format": "prettier --write \"**/*.js\"",
|
|
300
|
+
"test": "vitest",
|
|
301
|
+
"test:run": "vitest run",
|
|
302
|
+
"test:coverage": "vitest run --coverage"
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### ESLint Config
|
|
308
|
+
|
|
309
|
+
- [x] Create `eslint.config.js` with ESLint 9 flat config - **DONE**
|
|
310
|
+
|
|
311
|
+
### Lint Fixes Needed
|
|
312
|
+
|
|
313
|
+
- [x] Fix undeclared variables - **DONE**
|
|
314
|
+
- [x] Remove unused variables - **DONE**
|
|
315
|
+
- [x] Consistent quote style (Prettier) - **DONE**
|
|
316
|
+
- [x] Consistent semicolons (Prettier) - **DONE**
|
|
317
|
+
|
|
318
|
+
## CI/CD
|
|
319
|
+
|
|
320
|
+
- [x] Add GitHub Actions workflow for tests - **DONE**
|
|
321
|
+
- [x] Add GitHub Actions workflow for linting - **DONE**
|
|
322
|
+
- [x] Run on PR and push to main - **DONE**
|
|
323
|
+
|
|
324
|
+
See `.github/workflows/ci.yml`
|
package/vitest.config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
include: ['test/**/*.test.js'],
|
|
8
|
+
setupFiles: ['./test/setup.js'],
|
|
9
|
+
coverage: {
|
|
10
|
+
provider: 'v8',
|
|
11
|
+
include: ['**/*.js'],
|
|
12
|
+
exclude: ['cli.js', 'shell.js', 'dist/**', 'test/**', 'vitest.config.js', 'eslint.config.js'],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
package/webpack.config.js
CHANGED
|
@@ -1,19 +1,35 @@
|
|
|
1
|
-
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
2
6
|
|
|
3
7
|
const mode = process.env.BUILD_MODE || 'development';
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
entry:
|
|
9
|
+
export default {
|
|
10
|
+
entry: './hsync-web.js',
|
|
7
11
|
output: {
|
|
8
12
|
path: path.resolve(__dirname, 'dist'),
|
|
9
|
-
filename: mode === 'development' ? 'hsync.js' : 'hsync.min.js'
|
|
13
|
+
filename: mode === 'development' ? 'hsync.js' : 'hsync.min.js',
|
|
14
|
+
library: {
|
|
15
|
+
name: 'hsync',
|
|
16
|
+
type: 'umd',
|
|
17
|
+
export: 'default',
|
|
18
|
+
},
|
|
19
|
+
globalObject: 'this',
|
|
10
20
|
},
|
|
11
21
|
mode,
|
|
22
|
+
optimization: {
|
|
23
|
+
usedExports: true,
|
|
24
|
+
sideEffects: true,
|
|
25
|
+
},
|
|
12
26
|
resolve: {
|
|
13
|
-
alias: {
|
|
14
|
-
}
|
|
27
|
+
alias: {},
|
|
15
28
|
},
|
|
16
29
|
node: {
|
|
17
30
|
global: true,
|
|
18
|
-
}
|
|
19
|
-
|
|
31
|
+
},
|
|
32
|
+
performance: {
|
|
33
|
+
hints: false,
|
|
34
|
+
},
|
|
35
|
+
};
|