@serve.zone/dcrouter 13.27.1 → 13.29.1

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 (69) hide show
  1. package/.smartconfig.json +32 -10
  2. package/dist_serve/bundle.js +930 -799
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.d.ts +9 -1
  5. package/dist_ts/classes.dcrouter.js +22 -7
  6. package/dist_ts/config/classes.gateway-client-manager.d.ts +22 -0
  7. package/dist_ts/config/classes.gateway-client-manager.js +101 -0
  8. package/dist_ts/config/classes.route-config-manager.js +8 -7
  9. package/dist_ts/config/index.d.ts +1 -0
  10. package/dist_ts/config/index.js +2 -1
  11. package/dist_ts/db/documents/classes.gateway-client.doc.d.ts +18 -0
  12. package/dist_ts/db/documents/classes.gateway-client.doc.js +133 -0
  13. package/dist_ts/db/documents/index.d.ts +1 -0
  14. package/dist_ts/db/documents/index.js +2 -1
  15. package/dist_ts/opsserver/classes.opsserver.js +4 -1
  16. package/dist_ts/opsserver/handlers/admin.handler.d.ts +21 -6
  17. package/dist_ts/opsserver/handlers/admin.handler.js +188 -29
  18. package/dist_ts/opsserver/handlers/certificate.handler.js +5 -1
  19. package/dist_ts/opsserver/handlers/target-profile.handler.js +3 -1
  20. package/dist_ts/opsserver/handlers/users.handler.js +2 -2
  21. package/dist_ts/opsserver/handlers/workhoster.handler.d.ts +4 -0
  22. package/dist_ts/opsserver/handlers/workhoster.handler.js +146 -16
  23. package/dist_ts/plugins.d.ts +2 -0
  24. package/dist_ts/plugins.js +4 -1
  25. package/dist_ts/vpn/classes.vpn-manager.d.ts +2 -0
  26. package/dist_ts/vpn/classes.vpn-manager.js +41 -20
  27. package/dist_ts_apiclient/classes.workhoster.d.ts +1 -0
  28. package/dist_ts_apiclient/classes.workhoster.js +5 -1
  29. package/dist_ts_interfaces/data/workhoster.d.ts +28 -3
  30. package/dist_ts_interfaces/requests/admin.d.ts +38 -0
  31. package/dist_ts_interfaces/requests/users.d.ts +2 -5
  32. package/dist_ts_interfaces/requests/workhoster.d.ts +83 -1
  33. package/dist_ts_web/00_commitinfo_data.js +1 -1
  34. package/dist_ts_web/appstate.d.ts +46 -0
  35. package/dist_ts_web/appstate.js +105 -1
  36. package/dist_ts_web/elements/access/ops-view-apitokens.js +2 -1
  37. package/dist_ts_web/elements/access/ops-view-gatewayclients.d.ts +15 -0
  38. package/dist_ts_web/elements/access/ops-view-gatewayclients.js +293 -0
  39. package/dist_ts_web/elements/domains/ops-view-certificates.d.ts +6 -0
  40. package/dist_ts_web/elements/domains/ops-view-certificates.js +155 -13
  41. package/dist_ts_web/elements/network/ops-view-routes.js +2 -1
  42. package/dist_ts_web/elements/ops-dashboard.d.ts +4 -0
  43. package/dist_ts_web/elements/ops-dashboard.js +102 -3
  44. package/dist_ts_web/router.js +3 -3
  45. package/package.json +15 -22
  46. package/ts/00_commitinfo_data.ts +1 -1
  47. package/ts/classes.dcrouter.ts +30 -6
  48. package/ts/config/classes.gateway-client-manager.ts +117 -0
  49. package/ts/config/classes.route-config-manager.ts +8 -6
  50. package/ts/config/index.ts +2 -1
  51. package/ts/db/documents/classes.gateway-client.doc.ts +54 -0
  52. package/ts/db/documents/index.ts +1 -0
  53. package/ts/opsserver/classes.opsserver.ts +3 -0
  54. package/ts/opsserver/handlers/admin.handler.ts +244 -32
  55. package/ts/opsserver/handlers/certificate.handler.ts +5 -0
  56. package/ts/opsserver/handlers/target-profile.handler.ts +2 -0
  57. package/ts/opsserver/handlers/users.handler.ts +1 -1
  58. package/ts/opsserver/handlers/workhoster.handler.ts +191 -17
  59. package/ts/plugins.ts +7 -0
  60. package/ts/vpn/classes.vpn-manager.ts +56 -25
  61. package/ts_apiclient/classes.workhoster.ts +8 -0
  62. package/ts_web/00_commitinfo_data.ts +1 -1
  63. package/ts_web/appstate.ts +160 -0
  64. package/ts_web/elements/access/ops-view-apitokens.ts +1 -0
  65. package/ts_web/elements/access/ops-view-gatewayclients.ts +250 -0
  66. package/ts_web/elements/domains/ops-view-certificates.ts +166 -11
  67. package/ts_web/elements/network/ops-view-routes.ts +1 -0
  68. package/ts_web/elements/ops-dashboard.ts +102 -0
  69. package/ts_web/router.ts +2 -2
@@ -45,6 +45,16 @@ export class WorkHosterHandler {
45
45
  throw new plugins.typedrequest.TypedResponseError('unauthorized');
46
46
  }
47
47
 
48
+ private async requireAdmin(request: { identity?: interfaces.data.IIdentity }): Promise<string> {
49
+ if (request.identity?.jwt) {
50
+ const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({
51
+ identity: request.identity,
52
+ });
53
+ if (isAdmin) return request.identity.userId;
54
+ }
55
+ throw new plugins.typedrequest.TypedResponseError('admin identity required');
56
+ }
57
+
48
58
  private registerHandlers(): void {
49
59
  this.typedrouter.addTypedHandler(
50
60
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetGatewayCapabilities>(
@@ -56,6 +66,122 @@ export class WorkHosterHandler {
56
66
  ),
57
67
  );
58
68
 
69
+ this.typedrouter.addTypedHandler(
70
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetGatewayClientContext>(
71
+ 'getGatewayClientContext',
72
+ async (dataArg) => {
73
+ const auth = await this.requireAuth(dataArg, 'gateway-clients:read');
74
+ return {
75
+ context: this.getGatewayClientContext(auth),
76
+ capabilities: this.getGatewayCapabilities(),
77
+ };
78
+ },
79
+ ),
80
+ );
81
+
82
+ this.typedrouter.addTypedHandler(
83
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListGatewayClients>(
84
+ 'listGatewayClients',
85
+ async (dataArg) => {
86
+ await this.requireAdmin(dataArg);
87
+ return { gatewayClients: await this.listManagedGatewayClients() };
88
+ },
89
+ ),
90
+ );
91
+
92
+ this.typedrouter.addTypedHandler(
93
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateGatewayClient>(
94
+ 'createGatewayClient',
95
+ async (dataArg) => {
96
+ const userId = await this.requireAdmin(dataArg);
97
+ const manager = this.opsServerRef.dcRouterRef.gatewayClientManager;
98
+ if (!manager) return { success: false, message: 'Gateway client management not initialized' };
99
+ try {
100
+ const gatewayClient = await manager.createClient({
101
+ id: dataArg.id,
102
+ type: dataArg.type,
103
+ name: dataArg.name,
104
+ description: dataArg.description,
105
+ hostnamePatterns: dataArg.hostnamePatterns,
106
+ allowedRouteTargets: dataArg.allowedRouteTargets,
107
+ capabilities: dataArg.capabilities,
108
+ createdBy: userId,
109
+ });
110
+ return { success: true, gatewayClient };
111
+ } catch (error) {
112
+ return { success: false, message: (error as Error).message };
113
+ }
114
+ },
115
+ ),
116
+ );
117
+
118
+ this.typedrouter.addTypedHandler(
119
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateGatewayClient>(
120
+ 'updateGatewayClient',
121
+ async (dataArg) => {
122
+ await this.requireAdmin(dataArg);
123
+ const manager = this.opsServerRef.dcRouterRef.gatewayClientManager;
124
+ if (!manager) return { success: false, message: 'Gateway client management not initialized' };
125
+ const gatewayClient = await manager.updateClient(dataArg.id, {
126
+ name: dataArg.name,
127
+ description: dataArg.description,
128
+ hostnamePatterns: dataArg.hostnamePatterns,
129
+ allowedRouteTargets: dataArg.allowedRouteTargets,
130
+ capabilities: dataArg.capabilities,
131
+ enabled: dataArg.enabled,
132
+ });
133
+ return gatewayClient
134
+ ? { success: true, gatewayClient }
135
+ : { success: false, message: 'Gateway client not found' };
136
+ },
137
+ ),
138
+ );
139
+
140
+ this.typedrouter.addTypedHandler(
141
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteGatewayClient>(
142
+ 'deleteGatewayClient',
143
+ async (dataArg) => {
144
+ await this.requireAdmin(dataArg);
145
+ const manager = this.opsServerRef.dcRouterRef.gatewayClientManager;
146
+ if (!manager) return { success: false, message: 'Gateway client management not initialized' };
147
+ const success = await manager.deleteClient(dataArg.id);
148
+ return { success, message: success ? undefined : 'Gateway client not found' };
149
+ },
150
+ ),
151
+ );
152
+
153
+ this.typedrouter.addTypedHandler(
154
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateGatewayClientToken>(
155
+ 'createGatewayClientToken',
156
+ async (dataArg) => {
157
+ const userId = await this.requireAdmin(dataArg);
158
+ const gatewayClient = await this.opsServerRef.dcRouterRef.gatewayClientManager?.getClient(dataArg.gatewayClientId);
159
+ const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
160
+ if (!gatewayClient || !gatewayClient.enabled) {
161
+ return { success: false, message: 'Gateway client not found or disabled' };
162
+ }
163
+ if (!tokenManager) {
164
+ return { success: false, message: 'Token management not initialized' };
165
+ }
166
+ const result = await tokenManager.createToken(
167
+ dataArg.name?.trim() || `${gatewayClient.name} Token`,
168
+ ['gateway-clients:read', 'gateway-clients:write'],
169
+ dataArg.expiresInDays ?? null,
170
+ userId,
171
+ {
172
+ role: 'gatewayClient',
173
+ scopes: ['gateway-clients:read', 'gateway-clients:write'],
174
+ gatewayClient: { type: gatewayClient.type, id: gatewayClient.id },
175
+ hostnamePatterns: gatewayClient.hostnamePatterns,
176
+ allowedRouteTargets: gatewayClient.allowedRouteTargets,
177
+ capabilities: gatewayClient.capabilities,
178
+ },
179
+ );
180
+ return { success: true, tokenId: result.id, tokenValue: result.rawToken };
181
+ },
182
+ ),
183
+ );
184
+
59
185
  this.typedrouter.addTypedHandler(
60
186
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetGatewayClientDomains>(
61
187
  'getGatewayClientDomains',
@@ -183,6 +309,30 @@ export class WorkHosterHandler {
183
309
  };
184
310
  }
185
311
 
312
+ private getGatewayClientContext(auth: TAuthContext): interfaces.data.IGatewayClientContext {
313
+ const policy = auth.token?.policy;
314
+ const role = auth.isAdmin ? 'admin' : policy?.role || 'operator';
315
+ return {
316
+ role,
317
+ scopes: auth.token?.scopes || ['*'],
318
+ gatewayClient: policy?.gatewayClient,
319
+ hostnamePatterns: policy?.hostnamePatterns || [],
320
+ allowedRouteTargets: policy?.allowedRouteTargets || [],
321
+ capabilities: policy?.capabilities || {},
322
+ };
323
+ }
324
+
325
+ private async listManagedGatewayClients(): Promise<interfaces.data.IGatewayClient[]> {
326
+ const manager = this.opsServerRef.dcRouterRef.gatewayClientManager;
327
+ if (!manager) return [];
328
+ const clients = await manager.listClients();
329
+ const tokens = this.opsServerRef.dcRouterRef.apiTokenManager?.listTokens() || [];
330
+ return clients.map((client) => ({
331
+ ...client,
332
+ tokenCount: tokens.filter((token) => token.policy?.gatewayClient?.id === client.id).length,
333
+ }));
334
+ }
335
+
186
336
  private buildExternalKey(ownership: interfaces.data.IWorkAppRouteOwnership): string {
187
337
  return [
188
338
  ownership.workHosterType,
@@ -212,15 +362,38 @@ export class WorkHosterHandler {
212
362
  return policyClient.id;
213
363
  }
214
364
 
215
- private assertGatewayClientOwnership(auth: TAuthContext, ownership: interfaces.data.IGatewayClientOwnership): void {
365
+ private resolveGatewayClientOwnership(
366
+ auth: TAuthContext,
367
+ ownership: interfaces.data.IGatewayClientOwnership,
368
+ ): Required<interfaces.data.IGatewayClientOwnership> {
216
369
  const policy = auth.token?.policy;
217
- if (!policy || policy.role !== 'gatewayClient') return;
218
- if (!policy.gatewayClient) {
219
- throw new plugins.typedrequest.TypedResponseError('gateway client token is missing gatewayClient binding');
370
+ if (policy?.role === 'gatewayClient') {
371
+ if (!policy.gatewayClient) {
372
+ throw new plugins.typedrequest.TypedResponseError('gateway client token is missing gatewayClient binding');
373
+ }
374
+ if (ownership.gatewayClientType && ownership.gatewayClientType !== policy.gatewayClient.type) {
375
+ throw new plugins.typedrequest.TypedResponseError('gateway client token cannot act for this ownership');
376
+ }
377
+ if (ownership.gatewayClientId && ownership.gatewayClientId !== policy.gatewayClient.id) {
378
+ throw new plugins.typedrequest.TypedResponseError('gateway client token cannot act for this ownership');
379
+ }
380
+ return {
381
+ gatewayClientType: policy.gatewayClient.type,
382
+ gatewayClientId: policy.gatewayClient.id,
383
+ appId: ownership.appId,
384
+ hostname: ownership.hostname,
385
+ };
220
386
  }
221
- if (ownership.gatewayClientType !== policy.gatewayClient.type || ownership.gatewayClientId !== policy.gatewayClient.id) {
222
- throw new plugins.typedrequest.TypedResponseError('gateway client token cannot act for this ownership');
387
+
388
+ if (!ownership.gatewayClientType || !ownership.gatewayClientId) {
389
+ throw new plugins.typedrequest.TypedResponseError('gateway client ownership is missing type or id');
223
390
  }
391
+ return ownership as Required<interfaces.data.IGatewayClientOwnership>;
392
+ }
393
+
394
+ private assertGatewayClientOwnership(auth: TAuthContext, ownership: Required<interfaces.data.IGatewayClientOwnership>): void {
395
+ const policy = auth.token?.policy;
396
+ if (!policy || policy.role !== 'gatewayClient') return;
224
397
  if (!this.matchesHostnamePatterns(ownership.hostname, policy.hostnamePatterns || [])) {
225
398
  throw new plugins.typedrequest.TypedResponseError('hostname is outside token policy');
226
399
  }
@@ -403,7 +576,8 @@ export class WorkHosterHandler {
403
576
  enabled?: boolean,
404
577
  deleteRoute?: boolean,
405
578
  ): Promise<interfaces.data.IGatewayClientRouteSyncResult> {
406
- this.assertGatewayClientOwnership(auth, ownership);
579
+ const resolvedOwnership = this.resolveGatewayClientOwnership(auth, ownership);
580
+ this.assertGatewayClientOwnership(auth, resolvedOwnership);
407
581
  this.assertRouteTargetsAllowed(auth, route);
408
582
 
409
583
  const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
@@ -411,7 +585,7 @@ export class WorkHosterHandler {
411
585
  return { success: false, message: 'Route management not initialized' };
412
586
  }
413
587
 
414
- const externalKey = this.buildGatewayClientExternalKey(ownership);
588
+ const externalKey = this.buildGatewayClientExternalKey(resolvedOwnership);
415
589
  const existingRoute = manager.findApiRouteByExternalKey(externalKey);
416
590
 
417
591
  if (deleteRoute) {
@@ -430,15 +604,15 @@ export class WorkHosterHandler {
430
604
 
431
605
  const metadata: interfaces.data.IRouteMetadata = {
432
606
  ownerType: 'gatewayClient',
433
- gatewayClientType: ownership.gatewayClientType,
434
- gatewayClientId: ownership.gatewayClientId,
435
- gatewayClientAppId: ownership.appId,
436
- workHosterType: ownership.gatewayClientType,
437
- workHosterId: ownership.gatewayClientId,
438
- workAppId: ownership.appId,
607
+ gatewayClientType: resolvedOwnership.gatewayClientType,
608
+ gatewayClientId: resolvedOwnership.gatewayClientId,
609
+ gatewayClientAppId: resolvedOwnership.appId,
610
+ workHosterType: resolvedOwnership.gatewayClientType,
611
+ workHosterId: resolvedOwnership.gatewayClientId,
612
+ workAppId: resolvedOwnership.appId,
439
613
  externalKey,
440
614
  };
441
- const normalizedRoute = this.normalizeGatewayClientRoute(route, ownership, externalKey);
615
+ const normalizedRoute = this.normalizeGatewayClientRoute(route, resolvedOwnership, externalKey);
442
616
 
443
617
  if (existingRoute) {
444
618
  const result = await manager.updateRoute(existingRoute.id, {
@@ -455,7 +629,7 @@ export class WorkHosterHandler {
455
629
  return { success: true, action: 'created', routeId };
456
630
  }
457
631
 
458
- private buildGatewayClientExternalKey(ownership: interfaces.data.IGatewayClientOwnership): string {
632
+ private buildGatewayClientExternalKey(ownership: Required<interfaces.data.IGatewayClientOwnership>): string {
459
633
  return [
460
634
  ownership.gatewayClientType,
461
635
  ownership.gatewayClientId,
@@ -478,7 +652,7 @@ export class WorkHosterHandler {
478
652
 
479
653
  private normalizeGatewayClientRoute(
480
654
  route: interfaces.data.IDcRouterRouteConfig,
481
- ownership: interfaces.data.IGatewayClientOwnership,
655
+ ownership: Required<interfaces.data.IGatewayClientOwnership>,
482
656
  externalKey: string,
483
657
  ): interfaces.data.IDcRouterRouteConfig {
484
658
  const normalizedRoute = { ...route };
package/ts/plugins.ts CHANGED
@@ -41,6 +41,13 @@ export {
41
41
  typedsocket,
42
42
  }
43
43
 
44
+ // @idp.global scope
45
+ import * as idpSdkServer from '@idp.global/sdk/server';
46
+
47
+ export {
48
+ idpSdkServer,
49
+ }
50
+
44
51
  // @push.rocks scope
45
52
  import * as projectinfo from '@push.rocks/projectinfo';
46
53
  import * as qenv from '@push.rocks/qenv';
@@ -111,6 +111,7 @@ export class VpnManager {
111
111
 
112
112
  const subnet = this.getSubnet();
113
113
  const wgListenPort = this.config.wgListenPort ?? 51820;
114
+ const serverEndpoint = this.getWireGuardServerEndpoint();
114
115
 
115
116
  const desiredForwardingMode = this.getDesiredForwardingMode(anyClientUsesHostIp);
116
117
  if (anyClientUsesHostIp && desiredForwardingMode === 'hybrid') {
@@ -133,21 +134,19 @@ export class VpnManager {
133
134
  : { default: 'forceTarget' as const, target: '127.0.0.1' };
134
135
 
135
136
  const serverConfig: plugins.smartvpn.IVpnServerConfig = {
136
- listenAddr: '0.0.0.0:0', // WS listener not strictly needed but required field
137
+ listenAddr: '127.0.0.1:0', // Required by smartvpn, unused in wireguard-only mode
137
138
  privateKey: this.serverKeys.noisePrivateKey,
138
139
  publicKey: this.serverKeys.noisePublicKey,
139
140
  subnet,
140
141
  dns: this.config.dns,
141
142
  forwardingMode: forwardingMode as any,
142
- transportMode: 'all',
143
+ transportMode: 'wireguard',
143
144
  wgPrivateKey: this.serverKeys.wgPrivateKey,
144
145
  wgListenPort,
145
146
  clients: clientEntries,
146
147
  socketForwardProxyProtocol: !isBridge,
147
148
  destinationPolicy: this.getServerDestinationPolicy(forwardingMode, defaultDestinationPolicy),
148
- serverEndpoint: this.config.serverEndpoint
149
- ? `${this.config.serverEndpoint}:${wgListenPort}`
150
- : undefined,
149
+ serverEndpoint,
151
150
  clientAllowedIPs: [subnet],
152
151
  // Bridge-specific config
153
152
  ...(isBridge ? {
@@ -187,7 +186,7 @@ export class VpnManager {
187
186
  } catch {
188
187
  // Ignore stop errors
189
188
  }
190
- this.vpnServer.stop();
189
+ await this.vpnServer.stop();
191
190
  this.vpnServer = undefined;
192
191
  }
193
192
  this.resolvedForwardingMode = undefined;
@@ -244,14 +243,10 @@ export class VpnManager {
244
243
  vlanId: doc.vlanId,
245
244
  });
246
245
 
247
- // Override AllowedIPs with per-client values based on target profiles
248
- if (this.config.getClientAllowedIPs && bundle.wireguardConfig) {
249
- const allowedIPs = await this.config.getClientAllowedIPs(doc.targetProfileIds || []);
250
- bundle.wireguardConfig = bundle.wireguardConfig.replace(
251
- /AllowedIPs\s*=\s*.+/,
252
- `AllowedIPs = ${allowedIPs.join(', ')}`,
253
- );
254
- }
246
+ bundle.wireguardConfig = await this.rewriteWireGuardAllowedIPs(
247
+ bundle.wireguardConfig,
248
+ doc.targetProfileIds || [],
249
+ );
255
250
 
256
251
  // Persist client entry (including WG private key for export/QR)
257
252
  doc.clientId = bundle.entry.clientId;
@@ -381,9 +376,13 @@ export class VpnManager {
381
376
  public async rotateClientKey(clientId: string): Promise<plugins.smartvpn.IClientConfigBundle> {
382
377
  if (!this.vpnServer) throw new Error('VPN server not running');
383
378
  const bundle = await this.vpnServer.rotateClientKey(clientId);
379
+ const client = this.clients.get(clientId);
380
+ bundle.wireguardConfig = await this.rewriteWireGuardAllowedIPs(
381
+ bundle.wireguardConfig,
382
+ client?.targetProfileIds || [],
383
+ );
384
384
 
385
385
  // Update persisted entry with new keys (including private key for export/QR)
386
- const client = this.clients.get(clientId);
387
386
  if (client) {
388
387
  client.noisePublicKey = bundle.entry.publicKey;
389
388
  client.wgPublicKey = bundle.entry.wgPublicKey || '';
@@ -414,15 +413,7 @@ export class VpnManager {
414
413
  );
415
414
  }
416
415
 
417
- // Override AllowedIPs with per-client values based on target profiles
418
- if (this.config.getClientAllowedIPs) {
419
- const profileIds = persisted?.targetProfileIds || [];
420
- const allowedIPs = await this.config.getClientAllowedIPs(profileIds);
421
- config = config.replace(
422
- /AllowedIPs\s*=\s*.+/,
423
- `AllowedIPs = ${allowedIPs.join(', ')}`,
424
- );
425
- }
416
+ config = await this.rewriteWireGuardAllowedIPs(config, persisted?.targetProfileIds || []);
426
417
  }
427
418
 
428
419
  return config;
@@ -515,6 +506,46 @@ export class VpnManager {
515
506
  }
516
507
  }
517
508
 
509
+ private getWireGuardServerEndpoint(): string {
510
+ const endpoint = this.config.serverEndpoint?.trim();
511
+ if (!endpoint) {
512
+ throw new Error('vpnConfig.serverEndpoint is required when VPN is enabled');
513
+ }
514
+ if (endpoint.includes('://') || endpoint.includes('/')) {
515
+ throw new Error('vpnConfig.serverEndpoint must be a host or host:port, not a URL');
516
+ }
517
+
518
+ const host = endpoint.includes(':') ? endpoint.split(':')[0] : endpoint;
519
+ const lowerHost = host.toLowerCase();
520
+ if (
521
+ lowerHost === 'localhost'
522
+ || lowerHost === '0.0.0.0'
523
+ || lowerHost.startsWith('127.')
524
+ ) {
525
+ throw new Error('vpnConfig.serverEndpoint must be reachable by VPN clients');
526
+ }
527
+
528
+ return endpoint.includes(':')
529
+ ? endpoint
530
+ : `${endpoint}:${this.config.wgListenPort ?? 51820}`;
531
+ }
532
+
533
+ private async rewriteWireGuardAllowedIPs(
534
+ wireguardConfig: string,
535
+ targetProfileIds: string[],
536
+ ): Promise<string> {
537
+ if (!this.config.getClientAllowedIPs) return wireguardConfig;
538
+
539
+ const allowedIPs = await this.config.getClientAllowedIPs(targetProfileIds);
540
+ const effectiveAllowedIPs = allowedIPs.length ? allowedIPs : [this.getSubnet()];
541
+ const allowedLine = `AllowedIPs = ${effectiveAllowedIPs.join(', ')}`;
542
+
543
+ if (/^AllowedIPs\s*=.*$/m.test(wireguardConfig)) {
544
+ return wireguardConfig.replace(/^AllowedIPs\s*=.*$/m, allowedLine);
545
+ }
546
+ return `${wireguardConfig.trimEnd()}\n${allowedLine}\n`;
547
+ }
548
+
518
549
  // ── Private helpers ────────────────────────────────────────────────────
519
550
 
520
551
  private async loadOrGenerateServerKeys(): Promise<VpnServerKeysDoc> {
@@ -532,7 +563,7 @@ export class VpnManager {
532
563
 
533
564
  const noiseKeys = await tempServer.generateKeypair();
534
565
  const wgKeys = await tempServer.generateWgKeypair();
535
- tempServer.stop();
566
+ await tempServer.stop();
536
567
 
537
568
  const doc = stored || new VpnServerKeysDoc();
538
569
  doc.noisePrivateKey = noiseKeys.privateKey;
@@ -12,6 +12,14 @@ export class WorkHosterManager {
12
12
  return response.capabilities;
13
13
  }
14
14
 
15
+ public async getGatewayClientContext(): Promise<interfaces.data.IGatewayClientContext> {
16
+ const response = await this.clientRef.request<interfaces.requests.IReq_GetGatewayClientContext>(
17
+ 'getGatewayClientContext',
18
+ this.clientRef.buildRequestPayload() as any,
19
+ );
20
+ return response.context;
21
+ }
22
+
15
23
  public async getDomains(): Promise<interfaces.data.IWorkHosterDomain[]> {
16
24
  const response = await this.clientRef.request<interfaces.requests.IReq_GetWorkHosterDomains>(
17
25
  'getWorkHosterDomains',
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.27.1',
6
+ version: '13.29.1',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -10,6 +10,8 @@ export interface ILoginState {
10
10
  isLoggedIn: boolean;
11
11
  }
12
12
 
13
+ export type IAdminBootstrapStatus = interfaces.requests.IReq_GetAdminBootstrapStatus['response'];
14
+
13
15
  export interface IStatsState {
14
16
  serverStats: interfaces.data.IServerStats | null;
15
17
  emailStats: interfaces.data.IEmailStats | null;
@@ -285,6 +287,7 @@ export interface IRouteManagementState {
285
287
  mergedRoutes: interfaces.data.IMergedRoute[];
286
288
  warnings: interfaces.data.IRouteWarning[];
287
289
  apiTokens: interfaces.data.IApiTokenInfo[];
290
+ gatewayClients: interfaces.data.IGatewayClient[];
288
291
  isLoading: boolean;
289
292
  error: string | null;
290
293
  lastUpdated: number;
@@ -296,6 +299,7 @@ export const routeManagementStatePart = await appState.getStatePart<IRouteManage
296
299
  mergedRoutes: [],
297
300
  warnings: [],
298
301
  apiTokens: [],
302
+ gatewayClients: [],
299
303
  isLoading: false,
300
304
  error: null,
301
305
  lastUpdated: 0,
@@ -310,7 +314,11 @@ export const routeManagementStatePart = await appState.getStatePart<IRouteManage
310
314
  export interface IUser {
311
315
  id: string;
312
316
  username: string;
317
+ email?: string;
318
+ name?: string;
313
319
  role: string;
320
+ status?: 'active' | 'disabled';
321
+ authSources?: Array<'local' | 'idp.global'>;
314
322
  }
315
323
 
316
324
  export interface IUsersState {
@@ -349,6 +357,7 @@ const getActionContext = (): IActionContext => {
349
357
  export const loginAction = loginStatePart.createAction<{
350
358
  username: string;
351
359
  password: string;
360
+ authSource?: interfaces.requests.TAdminLoginAuthSource;
352
361
  }>(async (statePartArg, dataArg): Promise<ILoginState> => {
353
362
  const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
354
363
  interfaces.requests.IReq_AdminLoginWithUsernameAndPassword
@@ -358,6 +367,7 @@ export const loginAction = loginStatePart.createAction<{
358
367
  const response = await typedRequest.fire({
359
368
  username: dataArg.username,
360
369
  password: dataArg.password,
370
+ authSource: dataArg.authSource,
361
371
  });
362
372
 
363
373
  if (response.identity) {
@@ -373,6 +383,47 @@ export const loginAction = loginStatePart.createAction<{
373
383
  }
374
384
  });
375
385
 
386
+ export async function getAdminBootstrapStatus(): Promise<IAdminBootstrapStatus> {
387
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
388
+ interfaces.requests.IReq_GetAdminBootstrapStatus
389
+ >('/typedrequest', 'getAdminBootstrapStatus');
390
+
391
+ return request.fire({});
392
+ }
393
+
394
+ export async function createInitialAdminUser(optionsArg: {
395
+ email: string;
396
+ name?: string;
397
+ password: string;
398
+ enableIdpGlobalAuth?: boolean;
399
+ }) {
400
+ const context = getActionContext();
401
+ if (!context.identity) {
402
+ throw new Error('No identity available for admin bootstrap');
403
+ }
404
+
405
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
406
+ interfaces.requests.IReq_CreateInitialAdminUser
407
+ >('/typedrequest', 'createInitialAdminUser');
408
+
409
+ const response = await request.fire({
410
+ identity: context.identity,
411
+ email: optionsArg.email,
412
+ name: optionsArg.name,
413
+ password: optionsArg.password,
414
+ enableIdpGlobalAuth: optionsArg.enableIdpGlobalAuth,
415
+ });
416
+
417
+ if (response.identity) {
418
+ loginStatePart.setState({
419
+ identity: response.identity,
420
+ isLoggedIn: true,
421
+ });
422
+ }
423
+
424
+ return response;
425
+ }
426
+
376
427
  // Logout Action — always clears state, even if identity is expired/missing
377
428
  export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
378
429
  const context = getActionContext();
@@ -2477,6 +2528,115 @@ export const fetchApiTokensAction = routeManagementStatePart.createAction(async
2477
2528
  }
2478
2529
  });
2479
2530
 
2531
+ export const fetchGatewayClientsAction = routeManagementStatePart.createAction(async (statePartArg): Promise<IRouteManagementState> => {
2532
+ const context = getActionContext();
2533
+ const currentState = statePartArg.getState()!;
2534
+ if (!context.identity) return currentState;
2535
+
2536
+ try {
2537
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2538
+ interfaces.requests.IReq_ListGatewayClients
2539
+ >('/typedrequest', 'listGatewayClients');
2540
+ const response = await request.fire({ identity: context.identity });
2541
+ return {
2542
+ ...currentState,
2543
+ gatewayClients: response.gatewayClients,
2544
+ error: null,
2545
+ lastUpdated: Date.now(),
2546
+ };
2547
+ } catch (error) {
2548
+ return {
2549
+ ...currentState,
2550
+ error: error instanceof Error ? error.message : 'Failed to fetch gateway clients',
2551
+ };
2552
+ }
2553
+ });
2554
+
2555
+ export async function createGatewayClient(data: {
2556
+ id?: string;
2557
+ type: interfaces.data.IGatewayClient['type'];
2558
+ name: string;
2559
+ description?: string;
2560
+ hostnamePatterns?: string[];
2561
+ allowedRouteTargets?: interfaces.data.IGatewayClient['allowedRouteTargets'];
2562
+ }) {
2563
+ const context = getActionContext();
2564
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2565
+ interfaces.requests.IReq_CreateGatewayClient
2566
+ >('/typedrequest', 'createGatewayClient');
2567
+ return request.fire({
2568
+ identity: context.identity!,
2569
+ capabilities: {
2570
+ readDomains: true,
2571
+ readDnsRecords: true,
2572
+ syncRoutes: true,
2573
+ syncDnsRecords: false,
2574
+ requestCertificates: false,
2575
+ },
2576
+ ...data,
2577
+ });
2578
+ }
2579
+
2580
+ export const updateGatewayClientAction = routeManagementStatePart.createAction<{
2581
+ id: string;
2582
+ name?: string;
2583
+ description?: string;
2584
+ hostnamePatterns?: string[];
2585
+ allowedRouteTargets?: interfaces.data.IGatewayClient['allowedRouteTargets'];
2586
+ enabled?: boolean;
2587
+ }>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
2588
+ const context = getActionContext();
2589
+ const currentState = statePartArg.getState()!;
2590
+ try {
2591
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2592
+ interfaces.requests.IReq_UpdateGatewayClient
2593
+ >('/typedrequest', 'updateGatewayClient');
2594
+ await request.fire({ identity: context.identity!, ...dataArg });
2595
+ return await actionContext!.dispatch(fetchGatewayClientsAction, null);
2596
+ } catch (error) {
2597
+ return {
2598
+ ...currentState,
2599
+ error: error instanceof Error ? error.message : 'Failed to update gateway client',
2600
+ };
2601
+ }
2602
+ });
2603
+
2604
+ export const deleteGatewayClientAction = routeManagementStatePart.createAction<string>(
2605
+ async (statePartArg, gatewayClientId, actionContext): Promise<IRouteManagementState> => {
2606
+ const context = getActionContext();
2607
+ const currentState = statePartArg.getState()!;
2608
+ try {
2609
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2610
+ interfaces.requests.IReq_DeleteGatewayClient
2611
+ >('/typedrequest', 'deleteGatewayClient');
2612
+ await request.fire({ identity: context.identity!, id: gatewayClientId });
2613
+ return await actionContext!.dispatch(fetchGatewayClientsAction, null);
2614
+ } catch (error) {
2615
+ return {
2616
+ ...currentState,
2617
+ error: error instanceof Error ? error.message : 'Failed to delete gateway client',
2618
+ };
2619
+ }
2620
+ },
2621
+ );
2622
+
2623
+ export async function createGatewayClientToken(
2624
+ gatewayClientId: string,
2625
+ name?: string,
2626
+ expiresInDays?: number | null,
2627
+ ) {
2628
+ const context = getActionContext();
2629
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2630
+ interfaces.requests.IReq_CreateGatewayClientToken
2631
+ >('/typedrequest', 'createGatewayClientToken');
2632
+ return request.fire({
2633
+ identity: context.identity!,
2634
+ gatewayClientId,
2635
+ name,
2636
+ expiresInDays,
2637
+ });
2638
+ }
2639
+
2480
2640
  // Users (read-only list)
2481
2641
  export const fetchUsersAction = usersStatePart.createAction(async (statePartArg): Promise<IUsersState> => {
2482
2642
  const context = getActionContext();
@@ -20,6 +20,7 @@ export class OpsViewApiTokens extends DeesElement {
20
20
  mergedRoutes: [],
21
21
  warnings: [],
22
22
  apiTokens: [],
23
+ gatewayClients: [],
23
24
  isLoading: false,
24
25
  error: null,
25
26
  lastUpdated: 0,