@push.rocks/smartvpn 1.6.0 → 1.8.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.plan.md ADDED
@@ -0,0 +1,459 @@
1
+ # Enterprise Auth & Client Management for SmartVPN
2
+
3
+ ## Context
4
+
5
+ SmartVPN's Noise NK mode currently allows **any client that knows the server's public key** to connect — no per-client identity or access control. The goal is to make SmartVPN enterprise-ready with:
6
+
7
+ 1. **Per-client cryptographic authentication** (Noise IK handshake)
8
+ 2. **Rich client definitions** with ACLs, rate limits, and priority
9
+ 3. **Hub-generated configs** — server generates typed SmartVPN client configs AND WireGuard .conf files from the same client definition
10
+ 4. **Top-notch DX** — one `createClient()` call gives you everything
11
+
12
+ **This is a breaking change.** No backward compatibility with the old NK anonymous mode.
13
+
14
+ ---
15
+
16
+ ## Design Overview
17
+
18
+ ### The Hub Model
19
+
20
+ The server acts as a **hub** that manages client definitions. Each client definition is the **single source of truth** from which both SmartVPN native configs and WireGuard configs are generated.
21
+
22
+ ```
23
+ Hub (Server)
24
+ └── Client Registry
25
+ ├── "alice-laptop" → SmartVPN config OR WireGuard .conf
26
+ ├── "bob-phone" → SmartVPN config OR WireGuard .conf
27
+ └── "office-gw" → SmartVPN config OR WireGuard .conf
28
+ ```
29
+
30
+ ### Authentication: NK → IK (Breaking Change)
31
+
32
+ **Old (removed):** `Noise_NK_25519_ChaChaPoly_BLAKE2s` — client is anonymous
33
+ **New (always):** `Noise_IK_25519_ChaChaPoly_BLAKE2s` — client presents its static key during handshake
34
+
35
+ IK is a 2-message handshake (same count as NK), so **the frame protocol stays identical**. Changes:
36
+ - `create_initiator()` now requires `(client_private_key, server_public_key)` — always
37
+ - `create_responder()` remains `(server_private_key)` — but now uses IK pattern
38
+ - After handshake, server extracts client's public key via `get_remote_static()` and verifies against registry
39
+ - Old NK functions are replaced, not kept alongside
40
+
41
+ **Every client must have a keypair. Every server must have a client registry.**
42
+
43
+ ---
44
+
45
+ ## Core Interface: `IClientEntry`
46
+
47
+ This is the server-side client definition — the central config object.
48
+ Naming and structure are aligned with SmartProxy's `IRouteConfig` / `IRouteSecurity` patterns.
49
+
50
+ ```typescript
51
+ export interface IClientEntry {
52
+ /** Human-readable client ID (e.g. "alice-laptop") */
53
+ clientId: string;
54
+
55
+ /** Client's Noise IK public key (base64) — for SmartVPN native transport */
56
+ publicKey: string;
57
+ /** Client's WireGuard public key (base64) — for WireGuard transport */
58
+ wgPublicKey?: string;
59
+
60
+ // ── Security (aligned with SmartProxy IRouteSecurity pattern) ─────────
61
+
62
+ security?: IClientSecurity;
63
+
64
+ // ── QoS ────────────────────────────────────────────────────────────────
65
+
66
+ /** Traffic priority (lower = higher priority, default: 100) */
67
+ priority?: number;
68
+
69
+ // ── Metadata (aligned with SmartProxy IRouteConfig pattern) ────────────
70
+
71
+ /** Whether this client is enabled (default: true) */
72
+ enabled?: boolean;
73
+ /** Tags for grouping (e.g. ["engineering", "office"]) */
74
+ tags?: string[];
75
+ /** Optional description */
76
+ description?: string;
77
+ /** Optional expiry (ISO 8601 timestamp, omit = never expires) */
78
+ expiresAt?: string;
79
+ }
80
+
81
+ /**
82
+ * Security settings per client — mirrors SmartProxy's IRouteSecurity structure.
83
+ * Uses the same ipAllowList/ipBlockList naming convention.
84
+ * Adds VPN-specific destination filtering (destinationAllowList/destinationBlockList).
85
+ */
86
+ export interface IClientSecurity {
87
+ /** Source IPs/CIDRs the client may connect FROM (empty = any).
88
+ * Supports: exact IP, CIDR, wildcard (192.168.1.*), ranges (1.1.1.1-1.1.1.5).
89
+ * Same format as SmartProxy's ipAllowList. */
90
+ ipAllowList?: string[];
91
+ /** Source IPs blocked — overrides ipAllowList (deny wins).
92
+ * Same format as SmartProxy's ipBlockList. */
93
+ ipBlockList?: string[];
94
+ /** Destination IPs/CIDRs the client may reach through the VPN (empty = all) */
95
+ destinationAllowList?: string[];
96
+ /** Destination IPs blocked — overrides destinationAllowList (deny wins) */
97
+ destinationBlockList?: string[];
98
+ /** Max concurrent connections from this client */
99
+ maxConnections?: number;
100
+ /** Per-client rate limiting */
101
+ rateLimit?: IClientRateLimit;
102
+ }
103
+
104
+ export interface IClientRateLimit {
105
+ /** Max throughput in bytes/sec */
106
+ bytesPerSec: number;
107
+ /** Burst allowance in bytes */
108
+ burstBytes: number;
109
+ }
110
+ ```
111
+
112
+ ### SmartProxy Alignment Notes
113
+
114
+ | Pattern | SmartProxy | SmartVPN |
115
+ |---------|-----------|---------|
116
+ | ACL naming | `ipAllowList` / `ipBlockList` | Same — `ipAllowList` / `ipBlockList` |
117
+ | Security grouping | `security: IRouteSecurity` sub-object | Same — `security: IClientSecurity` sub-object |
118
+ | Rate limit structure | `rateLimit: IRouteRateLimit` object | Same pattern — `rateLimit: IClientRateLimit` object |
119
+ | IP format support | Exact, CIDR, wildcard, ranges | Same formats |
120
+ | Metadata fields | `priority`, `tags`, `enabled`, `description` | Same fields |
121
+ | ACL evaluation | Block-first, then allow-list | Same — deny overrides allow |
122
+
123
+ ### ACL Evaluation Order
124
+
125
+ ```
126
+ 1. Check ipBlockList / destinationBlockList first (explicit deny wins)
127
+ 2. If denied, DROP
128
+ 3. Check ipAllowList / destinationAllowList (explicit allow)
129
+ 4. If ipAllowList is empty → allow any source
130
+ 5. If destinationAllowList is empty → allow all destinations
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Hub Config Generation
136
+
137
+ ### `createClient()` — The One-Call DX
138
+
139
+ When the hub creates a client, it:
140
+ 1. Generates a Noise IK keypair for the client
141
+ 2. Generates a WireGuard keypair for the client
142
+ 3. Allocates a VPN IP address
143
+ 4. Stores the `IClientEntry` in the registry
144
+ 5. Returns a **complete config bundle** with everything the client needs
145
+
146
+ ```typescript
147
+ export interface IClientConfigBundle {
148
+ /** The server-side client entry */
149
+ entry: IClientEntry;
150
+ /** Ready-to-use SmartVPN client config (typed object) */
151
+ smartvpnConfig: IVpnClientConfig;
152
+ /** Ready-to-use WireGuard .conf file content (string) */
153
+ wireguardConfig: string;
154
+ /** Client's private keys (ONLY returned at creation time, not stored server-side) */
155
+ secrets: {
156
+ noisePrivateKey: string;
157
+ wgPrivateKey: string;
158
+ };
159
+ }
160
+ ```
161
+
162
+ The `secrets` are returned **only at creation time** — the server stores only public keys.
163
+
164
+ ### `exportClientConfig()` — Re-export (without secrets)
165
+
166
+ ```typescript
167
+ exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): IVpnClientConfig | string
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Updated `IVpnServerConfig`
173
+
174
+ ```typescript
175
+ export interface IVpnServerConfig {
176
+ listenAddr: string;
177
+ tlsCert?: string;
178
+ tlsKey?: string;
179
+ privateKey: string; // Server's Noise static private key (base64)
180
+ publicKey: string; // Server's Noise static public key (base64)
181
+ subnet: string;
182
+ dns?: string[];
183
+ mtu?: number;
184
+ keepaliveIntervalSecs?: number;
185
+ enableNat?: boolean;
186
+ defaultRateLimitBytesPerSec?: number;
187
+ defaultBurstBytes?: number;
188
+ transportMode?: 'websocket' | 'quic' | 'both' | 'wireguard';
189
+ quicListenAddr?: string;
190
+ quicIdleTimeoutSecs?: number;
191
+ wgListenPort?: number;
192
+ wgPeers?: IWgPeerConfig[]; // Keep for raw WG mode
193
+
194
+ /** Pre-registered clients — REQUIRED for SmartVPN native transport */
195
+ clients: IClientEntry[];
196
+ }
197
+ ```
198
+
199
+ Note: `clients` is now **required** (not optional), and there is no `authMode` field — IK is always used.
200
+
201
+ ---
202
+
203
+ ## Updated `IVpnClientConfig`
204
+
205
+ ```typescript
206
+ export interface IVpnClientConfig {
207
+ serverUrl: string;
208
+ serverPublicKey: string;
209
+ /** Client's Noise IK private key (base64) — REQUIRED for SmartVPN native transport */
210
+ clientPrivateKey: string;
211
+ /** Client's Noise IK public key (base64) — for reference/display */
212
+ clientPublicKey: string;
213
+ dns?: string[];
214
+ mtu?: number;
215
+ keepaliveIntervalSecs?: number;
216
+ transport?: 'auto' | 'websocket' | 'quic' | 'wireguard';
217
+ serverCertHash?: string;
218
+ // WireGuard fields unchanged...
219
+ wgPrivateKey?: string;
220
+ wgAddress?: string;
221
+ wgAddressPrefix?: number;
222
+ wgPresharedKey?: string;
223
+ wgPersistentKeepalive?: number;
224
+ wgEndpoint?: string;
225
+ wgAllowedIps?: string[];
226
+ }
227
+ ```
228
+
229
+ Note: `clientPrivateKey` and `clientPublicKey` are now **required** (not optional) for non-WireGuard transports.
230
+
231
+ ---
232
+
233
+ ## New IPC Commands
234
+
235
+ Added to `TVpnServerCommands`:
236
+
237
+ | Command | Params | Result | Description |
238
+ |---------|--------|--------|-------------|
239
+ | `createClient` | `{ client: Partial<IClientEntry> }` | `IClientConfigBundle` | Create client, generate keypairs, assign IP, return full config bundle |
240
+ | `removeClient` | `{ clientId: string }` | `void` | Remove from registry + disconnect if connected |
241
+ | `getClient` | `{ clientId: string }` | `IClientEntry` | Get a single client entry |
242
+ | `listRegisteredClients` | `{}` | `{ clients: IClientEntry[] }` | List all registered clients |
243
+ | `updateClient` | `{ clientId: string, update: Partial<IClientEntry> }` | `void` | Update ACLs, rate limits, tags, etc. |
244
+ | `enableClient` | `{ clientId: string }` | `void` | Enable a disabled client |
245
+ | `disableClient` | `{ clientId: string }` | `void` | Disable (but don't delete) |
246
+ | `rotateClientKey` | `{ clientId: string }` | `IClientConfigBundle` | New keypairs, return fresh config bundle |
247
+ | `exportClientConfig` | `{ clientId: string, format: 'smartvpn' \| 'wireguard' }` | `{ config: string }` | Re-export config (without secrets) |
248
+ | `generateClientKeypair` | `{}` | `IVpnKeypair` | Generate a standalone Noise IK keypair |
249
+
250
+ ---
251
+
252
+ ## Implementation Plan
253
+
254
+ ### Phase 1: Rust — Crypto (Replace NK with IK)
255
+
256
+ **File: `rust/src/crypto.rs`**
257
+
258
+ - Change `NOISE_PATTERN` from NK to IK: `"Noise_IK_25519_ChaChaPoly_BLAKE2s"`
259
+ - Replace `create_initiator(server_public_key)` → `create_initiator(client_private_key, server_public_key)`
260
+ - `create_responder(private_key)` stays the same signature (IK responder only needs its own key)
261
+ - After handshake, `get_remote_static()` on the responder returns the client's public key
262
+ - Update `perform_handshake()` to pass client keypair
263
+ - Update all tests
264
+
265
+ ### Phase 2: Rust — Client Registry module
266
+
267
+ **New file: `rust/src/client_registry.rs`**
268
+ **Modify: `rust/src/lib.rs`** — add `pub mod client_registry;`
269
+
270
+ ```rust
271
+ pub struct ClientEntry {
272
+ pub client_id: String,
273
+ pub public_key: String,
274
+ pub wg_public_key: Option<String>,
275
+ pub security: Option<ClientSecurity>,
276
+ pub priority: Option<u32>,
277
+ pub enabled: Option<bool>,
278
+ pub tags: Option<Vec<String>>,
279
+ pub description: Option<String>,
280
+ pub expires_at: Option<String>,
281
+ pub assigned_ip: Option<String>,
282
+ }
283
+
284
+ /// Mirrors IClientSecurity — aligned with SmartProxy's IRouteSecurity
285
+ pub struct ClientSecurity {
286
+ pub ip_allow_list: Option<Vec<String>>,
287
+ pub ip_block_list: Option<Vec<String>>,
288
+ pub destination_allow_list: Option<Vec<String>>,
289
+ pub destination_block_list: Option<Vec<String>>,
290
+ pub max_connections: Option<u32>,
291
+ pub rate_limit: Option<ClientRateLimit>,
292
+ }
293
+
294
+ pub struct ClientRateLimit {
295
+ pub bytes_per_sec: u64,
296
+ pub burst_bytes: u64,
297
+ }
298
+
299
+ pub struct ClientRegistry {
300
+ entries: HashMap<String, ClientEntry>, // keyed by clientId
301
+ key_index: HashMap<String, String>, // publicKey → clientId (fast lookup)
302
+ }
303
+ ```
304
+
305
+ Methods: `add`, `remove`, `get_by_id`, `get_by_key`, `update`, `list`, `is_authorized` (enabled + not expired + key exists), `rotate_key`.
306
+
307
+ ### Phase 3: Rust — ACL enforcement module
308
+
309
+ **New file: `rust/src/acl.rs`**
310
+ **Modify: `rust/src/lib.rs`** — add `pub mod acl;`
311
+
312
+ ```rust
313
+ /// IP matching supports: exact, CIDR, wildcard, ranges — same as SmartProxy's IpMatcher
314
+ pub fn check_acl(security: &ClientSecurity, src_ip: Ipv4Addr, dst_ip: Ipv4Addr) -> AclResult {
315
+ // 1. Check ip_block_list / destination_block_list (deny overrides)
316
+ // 2. Check ip_allow_list / destination_allow_list (explicit allow)
317
+ // 3. Empty list = allow all
318
+ }
319
+ ```
320
+
321
+ Called in `server.rs` packet loop after decryption, before forwarding.
322
+
323
+ ### Phase 4: Rust — Server changes
324
+
325
+ **File: `rust/src/server.rs`**
326
+
327
+ - Add `clients: Option<Vec<ClientEntry>>` to `ServerConfig`
328
+ - Add `client_registry: RwLock<ClientRegistry>` to `ServerState` (no `auth_mode` — always IK)
329
+ - Modify `handle_client_connection()`:
330
+ - Always use `create_responder()` (now IK pattern)
331
+ - Call `get_remote_static()` **before** `into_transport_mode()` to get client's public key
332
+ - Verify against registry — reject unauthorized clients with Disconnect frame
333
+ - Use registry entry for rate limits (overrides server defaults)
334
+ - In packet loop: call `acl::check_acl()` on decrypted packets
335
+ - Add `ClientInfo.authenticated_key: String` and `ClientInfo.registered_client_id: String` (no longer optional)
336
+ - Add methods: `create_client()`, `remove_client()`, `update_client()`, `list_registered_clients()`, `rotate_client_key()`, `export_client_config()`
337
+
338
+ ### Phase 5: Rust — Client changes
339
+
340
+ **File: `rust/src/client.rs`**
341
+
342
+ - Add `client_private_key: String` to `ClientConfig` (required, not optional)
343
+ - `connect()` always uses `create_initiator(client_private_key, server_public_key)` (IK)
344
+
345
+ ### Phase 6: Rust — Management IPC handlers
346
+
347
+ **File: `rust/src/management.rs`**
348
+
349
+ Add handlers for all 10 new IPC commands following existing patterns.
350
+
351
+ ### Phase 7: TypeScript — Interfaces
352
+
353
+ **File: `ts/smartvpn.interfaces.ts`**
354
+
355
+ - Add `IClientEntry` interface
356
+ - Add `IClientConfigBundle` interface
357
+ - Update `IVpnServerConfig`: add required `clients: IClientEntry[]`
358
+ - Update `IVpnClientConfig`: add required `clientPrivateKey: string`, `clientPublicKey: string`
359
+ - Update `IVpnClientInfo`: add `authenticatedKey: string`, `registeredClientId: string`
360
+ - Add new commands to `TVpnServerCommands`
361
+
362
+ ### Phase 8: TypeScript — VpnServer class methods
363
+
364
+ **File: `ts/smartvpn.classes.vpnserver.ts`**
365
+
366
+ Add methods:
367
+ - `createClient(opts)` → `IClientConfigBundle`
368
+ - `removeClient(clientId)` → `void`
369
+ - `getClient(clientId)` → `IClientEntry`
370
+ - `listRegisteredClients()` → `IClientEntry[]`
371
+ - `updateClient(clientId, update)` → `void`
372
+ - `enableClient(clientId)` / `disableClient(clientId)`
373
+ - `rotateClientKey(clientId)` → `IClientConfigBundle`
374
+ - `exportClientConfig(clientId, format)` → `string | IVpnClientConfig`
375
+
376
+ ### Phase 9: TypeScript — Config validation
377
+
378
+ **File: `ts/smartvpn.classes.vpnconfig.ts`**
379
+
380
+ - Server config: validate `clients` present, each entry has valid `clientId` + `publicKey`
381
+ - Client config: validate `clientPrivateKey` and `clientPublicKey` present for non-WG transports
382
+ - Validate CIDRs in ACL fields
383
+
384
+ ### Phase 10: TypeScript — Hub config generation
385
+
386
+ **File: `ts/smartvpn.classes.wgconfig.ts`** (extend existing)
387
+
388
+ Add `generateClientConfigFromEntry(entry, serverConfig)` — produces WireGuard .conf from `IClientEntry`.
389
+
390
+ ### Phase 11: Update existing tests
391
+
392
+ All existing tests that use the old NK handshake or old config shapes need updating:
393
+ - Rust tests in `crypto.rs`, `server.rs`, `client.rs`
394
+ - TS tests in `test/test.vpnconfig.node.ts`, `test/test.flowcontrol.node.ts`, etc.
395
+ - Tests now must provide client keypairs and client registry entries
396
+
397
+ ---
398
+
399
+ ## DX Highlights
400
+
401
+ 1. **One call to create a client:**
402
+ ```typescript
403
+ const bundle = await server.createClient({ clientId: 'alice-laptop', tags: ['engineering'] });
404
+ // bundle.smartvpnConfig — typed SmartVPN client config
405
+ // bundle.wireguardConfig — standard WireGuard .conf string
406
+ // bundle.secrets — private keys, shown only at creation time
407
+ ```
408
+
409
+ 2. **Typed config objects throughout** — no raw strings or JSON blobs
410
+
411
+ 3. **Dual transport from same definition** — register once, connect via SmartVPN or WireGuard
412
+
413
+ 4. **ACLs are deny-overrides-allow** — intuitive enterprise model
414
+
415
+ 5. **Hot management** — add/remove/update/disable clients at runtime
416
+
417
+ 6. **Key rotation** — `rotateClientKey()` generates new keys and returns a fresh config bundle
418
+
419
+ ---
420
+
421
+ ## Verification Plan
422
+
423
+ 1. **Rust unit tests:**
424
+ - `crypto.rs`: IK handshake roundtrip, `get_remote_static()` returns correct key, wrong key fails
425
+ - `client_registry.rs`: CRUD, `is_authorized` with enabled/disabled/expired
426
+ - `acl.rs`: allow/deny logic, empty lists, deny-overrides-allow
427
+
428
+ 2. **Rust integration tests:**
429
+ - Server accepts authorized client
430
+ - Server rejects unknown client public key
431
+ - ACL filtering drops packets to blocked destinations
432
+ - Runtime `createClient` / `removeClient` works
433
+ - Disabled client rejected at handshake
434
+
435
+ 3. **TypeScript tests:**
436
+ - Config validation with required client fields
437
+ - `createClient()` returns valid bundle with both formats
438
+ - `exportClientConfig()` generates valid WireGuard .conf
439
+ - Full IPC roundtrip: create client → connect → traffic → disconnect
440
+
441
+ 4. **Build:** `pnpm build` (TS + Rust), `cargo test`, `pnpm test`
442
+
443
+ ---
444
+
445
+ ## Key Files to Modify
446
+
447
+ | File | Changes |
448
+ |------|---------|
449
+ | `rust/src/crypto.rs` | Replace NK with IK pattern, update initiator signature |
450
+ | `rust/src/client_registry.rs` | **NEW** — client registry module |
451
+ | `rust/src/acl.rs` | **NEW** — ACL evaluation module |
452
+ | `rust/src/server.rs` | Registry integration, IK auth in handshake, ACL in packet loop |
453
+ | `rust/src/client.rs` | Required `client_private_key`, IK initiator |
454
+ | `rust/src/management.rs` | 10 new IPC command handlers |
455
+ | `rust/src/lib.rs` | Register new modules |
456
+ | `ts/smartvpn.interfaces.ts` | `IClientEntry`, `IClientConfigBundle`, updated configs & commands |
457
+ | `ts/smartvpn.classes.vpnserver.ts` | New hub methods |
458
+ | `ts/smartvpn.classes.vpnconfig.ts` | Updated validation rules |
459
+ | `ts/smartvpn.classes.wgconfig.ts` | Config generation from client entries |
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartvpn',
6
- version: '1.6.0',
6
+ version: '1.8.0',
7
7
  description: 'A VPN solution with TypeScript control plane and Rust data plane daemon'
8
8
  }
@@ -51,6 +51,15 @@ export class VpnConfig {
51
51
  if (!config.serverPublicKey) {
52
52
  throw new Error('VpnConfig: serverPublicKey is required');
53
53
  }
54
+ // Noise IK requires client keypair
55
+ if (!config.clientPrivateKey) {
56
+ throw new Error('VpnConfig: clientPrivateKey is required for Noise IK authentication');
57
+ }
58
+ VpnConfig.validateBase64Key(config.clientPrivateKey, 'clientPrivateKey');
59
+ if (!config.clientPublicKey) {
60
+ throw new Error('VpnConfig: clientPublicKey is required for Noise IK authentication');
61
+ }
62
+ VpnConfig.validateBase64Key(config.clientPublicKey, 'clientPublicKey');
54
63
  }
55
64
  if (config.mtu !== undefined && (config.mtu < 576 || config.mtu > 65535)) {
56
65
  throw new Error('VpnConfig: mtu must be between 576 and 65535');
@@ -116,6 +125,18 @@ export class VpnConfig {
116
125
  if (!VpnConfig.isValidSubnet(config.subnet)) {
117
126
  throw new Error(`VpnConfig: invalid subnet: ${config.subnet}`);
118
127
  }
128
+ // Validate client entries if provided
129
+ if (config.clients) {
130
+ for (const client of config.clients) {
131
+ if (!client.clientId) {
132
+ throw new Error('VpnConfig: client entry must have a clientId');
133
+ }
134
+ if (!client.publicKey) {
135
+ throw new Error(`VpnConfig: client '${client.clientId}' must have a publicKey`);
136
+ }
137
+ VpnConfig.validateBase64Key(client.publicKey, `client '${client.clientId}' publicKey`);
138
+ }
139
+ }
119
140
  }
120
141
  if (config.mtu !== undefined && (config.mtu < 576 || config.mtu > 65535)) {
121
142
  throw new Error('VpnConfig: mtu must be between 576 and 65535');
@@ -10,6 +10,8 @@ import type {
10
10
  IVpnClientTelemetry,
11
11
  IWgPeerConfig,
12
12
  IWgPeerInfo,
13
+ IClientEntry,
14
+ IClientConfigBundle,
13
15
  TVpnServerCommands,
14
16
  } from './smartvpn.interfaces.js';
15
17
 
@@ -152,6 +154,81 @@ export class VpnServer extends plugins.events.EventEmitter {
152
154
  return result.peers;
153
155
  }
154
156
 
157
+ // ── Client Registry (Hub) Methods ─────────────────────────────────────
158
+
159
+ /**
160
+ * Create a new client. Generates keypairs, assigns IP, returns full config bundle.
161
+ * The secrets (private keys) are only returned at creation time.
162
+ */
163
+ public async createClient(opts: Partial<IClientEntry>): Promise<IClientConfigBundle> {
164
+ return this.bridge.sendCommand('createClient', { client: opts });
165
+ }
166
+
167
+ /**
168
+ * Remove a registered client (also disconnects if connected).
169
+ */
170
+ public async removeClient(clientId: string): Promise<void> {
171
+ await this.bridge.sendCommand('removeClient', { clientId });
172
+ }
173
+
174
+ /**
175
+ * Get a registered client by ID.
176
+ */
177
+ public async getClient(clientId: string): Promise<IClientEntry> {
178
+ return this.bridge.sendCommand('getClient', { clientId });
179
+ }
180
+
181
+ /**
182
+ * List all registered clients.
183
+ */
184
+ public async listRegisteredClients(): Promise<IClientEntry[]> {
185
+ const result = await this.bridge.sendCommand('listRegisteredClients', {} as Record<string, never>);
186
+ return result.clients;
187
+ }
188
+
189
+ /**
190
+ * Update a registered client's fields (ACLs, tags, description, etc.).
191
+ */
192
+ public async updateClient(clientId: string, update: Partial<IClientEntry>): Promise<void> {
193
+ await this.bridge.sendCommand('updateClient', { clientId, update });
194
+ }
195
+
196
+ /**
197
+ * Enable a previously disabled client.
198
+ */
199
+ public async enableClient(clientId: string): Promise<void> {
200
+ await this.bridge.sendCommand('enableClient', { clientId });
201
+ }
202
+
203
+ /**
204
+ * Disable a client (also disconnects if connected).
205
+ */
206
+ public async disableClient(clientId: string): Promise<void> {
207
+ await this.bridge.sendCommand('disableClient', { clientId });
208
+ }
209
+
210
+ /**
211
+ * Rotate a client's keys. Returns a new config bundle with fresh keypairs.
212
+ */
213
+ public async rotateClientKey(clientId: string): Promise<IClientConfigBundle> {
214
+ return this.bridge.sendCommand('rotateClientKey', { clientId });
215
+ }
216
+
217
+ /**
218
+ * Export a client config (without secrets) in the specified format.
219
+ */
220
+ public async exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): Promise<string> {
221
+ const result = await this.bridge.sendCommand('exportClientConfig', { clientId, format });
222
+ return result.config;
223
+ }
224
+
225
+ /**
226
+ * Generate a standalone Noise IK keypair (not tied to a client).
227
+ */
228
+ public async generateClientKeypair(): Promise<IVpnKeypair> {
229
+ return this.bridge.sendCommand('generateClientKeypair', {} as Record<string, never>);
230
+ }
231
+
155
232
  /**
156
233
  * Stop the daemon bridge.
157
234
  */