@serve.zone/dcrouter 13.28.0 → 13.30.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.
Files changed (39) hide show
  1. package/.smartconfig.json +32 -10
  2. package/dist_serve/bundle.js +609 -592
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.d.ts +7 -0
  5. package/dist_ts/classes.dcrouter.js +12 -2
  6. package/dist_ts/config/classes.route-config-manager.js +8 -7
  7. package/dist_ts/opsserver/classes.opsserver.js +4 -1
  8. package/dist_ts/opsserver/handlers/admin.handler.d.ts +21 -6
  9. package/dist_ts/opsserver/handlers/admin.handler.js +188 -29
  10. package/dist_ts/opsserver/handlers/target-profile.handler.js +3 -1
  11. package/dist_ts/opsserver/handlers/users.handler.js +2 -2
  12. package/dist_ts/plugins.d.ts +2 -0
  13. package/dist_ts/plugins.js +4 -1
  14. package/dist_ts/vpn/classes.vpn-manager.d.ts +2 -0
  15. package/dist_ts/vpn/classes.vpn-manager.js +41 -20
  16. package/dist_ts_interfaces/requests/admin.d.ts +38 -0
  17. package/dist_ts_interfaces/requests/users.d.ts +2 -5
  18. package/dist_ts_web/00_commitinfo_data.js +1 -1
  19. package/dist_ts_web/appstate.d.ts +17 -0
  20. package/dist_ts_web/appstate.js +27 -1
  21. package/dist_ts_web/elements/ops-dashboard.d.ts +4 -0
  22. package/dist_ts_web/elements/ops-dashboard.js +100 -3
  23. package/package.json +27 -34
  24. package/readme.md +15 -3
  25. package/ts/00_commitinfo_data.ts +1 -1
  26. package/ts/classes.dcrouter.ts +20 -1
  27. package/ts/config/classes.route-config-manager.ts +8 -6
  28. package/ts/opsserver/classes.opsserver.ts +3 -0
  29. package/ts/opsserver/handlers/admin.handler.ts +244 -32
  30. package/ts/opsserver/handlers/target-profile.handler.ts +2 -0
  31. package/ts/opsserver/handlers/users.handler.ts +1 -1
  32. package/ts/plugins.ts +7 -0
  33. package/ts/readme.md +1 -1
  34. package/ts/vpn/classes.vpn-manager.ts +56 -25
  35. package/ts_apiclient/readme.md +4 -4
  36. package/ts_web/00_commitinfo_data.ts +1 -1
  37. package/ts_web/appstate.ts +49 -0
  38. package/ts_web/elements/ops-dashboard.ts +100 -0
  39. package/ts_web/readme.md +5 -3
@@ -8,19 +8,33 @@ export interface IJwtData {
8
8
  expiresAt: number;
9
9
  }
10
10
 
11
+ type TAdminUser = {
12
+ id: string;
13
+ username: string;
14
+ email?: string;
15
+ name?: string;
16
+ role: string;
17
+ status?: 'active' | 'disabled';
18
+ authSources?: Array<'local' | 'idp.global'>;
19
+ };
20
+
11
21
  export class AdminHandler {
12
22
  public typedrouter = new plugins.typedrequest.TypedRouter();
13
23
 
14
24
  // JWT instance
15
25
  public smartjwtInstance!: plugins.smartjwt.SmartJwt<IJwtData>;
16
26
 
17
- // Simple in-memory user storage (in production, use proper database)
27
+ // Ephemeral bootstrap users. Persisted accounts take over once an active admin exists.
18
28
  private users = new Map<string, {
19
29
  id: string;
20
30
  username: string;
21
31
  password: string;
22
32
  role: string;
23
33
  }>();
34
+
35
+ private accountStore?: plugins.idpSdkServer.SmartdataAccountStore;
36
+ private idpClient?: plugins.idpSdkServer.IdpGlobalServerClient;
37
+ private ownsIdpClient = false;
24
38
 
25
39
  constructor(private opsServerRef: OpsServer) {
26
40
  // Add this handler's router to the parent
@@ -32,6 +46,14 @@ export class AdminHandler {
32
46
  this.initializeDefaultUsers();
33
47
  this.registerHandlers();
34
48
  }
49
+
50
+ public async stop(): Promise<void> {
51
+ if (this.ownsIdpClient) {
52
+ await this.idpClient?.stop();
53
+ }
54
+ this.idpClient = undefined;
55
+ this.ownsIdpClient = false;
56
+ }
35
57
 
36
58
  private async initializeJwt(): Promise<void> {
37
59
  this.smartjwtInstance = new plugins.smartjwt.SmartJwt();
@@ -61,54 +83,120 @@ export class AdminHandler {
61
83
  }
62
84
 
63
85
  /**
64
- * Return a safe projection of the users Map — excludes password fields.
86
+ * Return a safe projection of the active user source — excludes password fields.
65
87
  * Used by UsersHandler to serve the admin-only listUsers endpoint.
66
88
  */
67
- public listUsers(): Array<{ id: string; username: string; role: string }> {
89
+ public async listUsers(): Promise<interfaces.requests.IAdminUserProjection[]> {
90
+ if (await this.hasPersistentAdminAccount()) {
91
+ const store = this.getAccountStore();
92
+ const accounts = await store!.listAccounts();
93
+ return accounts.map((accountArg) => this.accountToUser(accountArg));
94
+ }
95
+
68
96
  return Array.from(this.users.values()).map((user) => ({
69
97
  id: user.id,
70
98
  username: user.username,
71
99
  role: user.role,
72
100
  }));
73
101
  }
102
+
103
+ public async getBootstrapStatus(): Promise<interfaces.requests.IReq_GetAdminBootstrapStatus['response']> {
104
+ const dbEnabled = this.opsServerRef.dcRouterRef.options.dbConfig?.enabled !== false;
105
+ const store = this.getAccountStore();
106
+ const dbReady = !!store;
107
+ const hasPersistentAdmin = dbReady ? await store.hasActiveAdminAccount() : false;
108
+ return {
109
+ dbEnabled,
110
+ dbReady,
111
+ hasPersistentAdmin,
112
+ needsBootstrap: dbEnabled && dbReady && !hasPersistentAdmin,
113
+ ephemeralAdminAvailable: !hasPersistentAdmin,
114
+ idpGlobalConfigured: this.isIdpGlobalConfigured(),
115
+ };
116
+ }
117
+
118
+ public async createInitialAdminUser(optionsArg: {
119
+ email: string;
120
+ name?: string;
121
+ password: string;
122
+ enableIdpGlobalAuth?: boolean;
123
+ }): Promise<interfaces.requests.IReq_CreateInitialAdminUser['response']> {
124
+ const store = this.getAccountStore();
125
+ if (!store) {
126
+ throw new plugins.typedrequest.TypedResponseError('database is not ready');
127
+ }
128
+
129
+ if (await store.hasActiveAdminAccount()) {
130
+ throw new plugins.typedrequest.TypedResponseError('initial admin already exists');
131
+ }
132
+
133
+ const password = String(optionsArg.password || '');
134
+ if (!password) {
135
+ throw new plugins.typedrequest.TypedResponseError('password is required');
136
+ }
137
+
138
+ const email = String(optionsArg.email || '').trim();
139
+ const authSources: Array<'local' | 'idp.global'> = ['local'];
140
+ if (optionsArg.enableIdpGlobalAuth) {
141
+ authSources.push('idp.global');
142
+ }
143
+
144
+ try {
145
+ const account = await store.createAccount({
146
+ email,
147
+ name: String(optionsArg.name || '').trim() || email,
148
+ role: 'admin',
149
+ authSources,
150
+ password,
151
+ });
152
+ const user = this.accountToUser(account);
153
+ return {
154
+ success: true,
155
+ identity: await this.createIdentityForUser(user),
156
+ user,
157
+ };
158
+ } catch (error) {
159
+ throw new plugins.typedrequest.TypedResponseError((error as Error).message || 'failed to create initial admin');
160
+ }
161
+ }
74
162
 
75
163
  private registerHandlers(): void {
164
+ this.typedrouter.addTypedHandler(
165
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAdminBootstrapStatus>(
166
+ 'getAdminBootstrapStatus',
167
+ async (_dataArg) => this.getBootstrapStatus()
168
+ )
169
+ );
170
+
171
+ this.opsServerRef.adminRouter.addTypedHandler(
172
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateInitialAdminUser>(
173
+ 'createInitialAdminUser',
174
+ async (dataArg) => this.createInitialAdminUser({
175
+ email: dataArg.email,
176
+ name: dataArg.name,
177
+ password: dataArg.password,
178
+ enableIdpGlobalAuth: dataArg.enableIdpGlobalAuth,
179
+ })
180
+ )
181
+ );
182
+
76
183
  // Admin Login Handler
77
184
  this.typedrouter.addTypedHandler(
78
185
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
79
186
  'adminLoginWithUsernameAndPassword',
80
187
  async (dataArg) => {
81
188
  try {
82
- // Find user by username and password
83
- let user: { id: string; username: string; password: string; role: string } | null = null;
84
- for (const [_, userData] of this.users) {
85
- if (userData.username === dataArg.username && userData.password === dataArg.password) {
86
- user = userData;
87
- break;
88
- }
89
- }
90
-
189
+ const user = await this.authenticateUser({
190
+ username: dataArg.username,
191
+ password: dataArg.password,
192
+ authSource: dataArg.authSource,
193
+ });
91
194
  if (!user) {
92
195
  throw new plugins.typedrequest.TypedResponseError('login failed');
93
196
  }
94
-
95
- const expiresAtTimestamp = Date.now() + 3600 * 1000 * 24; // 24 hours
96
-
97
- const jwt = await this.smartjwtInstance.createJWT({
98
- userId: user.id,
99
- status: 'loggedIn',
100
- expiresAt: expiresAtTimestamp,
101
- });
102
-
197
+
103
198
  return {
104
- identity: {
105
- jwt,
106
- userId: user.id,
107
- name: user.username,
108
- expiresAt: expiresAtTimestamp,
109
- role: user.role,
110
- type: 'user',
111
- },
199
+ identity: await this.createIdentityForUser(user),
112
200
  };
113
201
  } catch (error) {
114
202
  if (error instanceof plugins.typedrequest.TypedResponseError) {
@@ -162,8 +250,7 @@ export class AdminHandler {
162
250
  };
163
251
  }
164
252
 
165
- // Find user
166
- const user = this.users.get(jwtData.userId);
253
+ const user = await this.resolveUser(jwtData.userId);
167
254
  if (!user) {
168
255
  return {
169
256
  valid: false,
@@ -175,7 +262,7 @@ export class AdminHandler {
175
262
  identity: {
176
263
  jwt: dataArg.identity.jwt,
177
264
  userId: user.id,
178
- name: user.username,
265
+ name: user.name || user.username,
179
266
  expiresAt: jwtData.expiresAt,
180
267
  role: user.role,
181
268
  type: 'user',
@@ -224,6 +311,15 @@ export class AdminHandler {
224
311
  return false;
225
312
  }
226
313
 
314
+ const user = await this.resolveUser(jwtData.userId);
315
+ if (!user) {
316
+ return false;
317
+ }
318
+
319
+ if (dataArg.identity.role && dataArg.identity.role !== user.role) {
320
+ return false;
321
+ }
322
+
227
323
  return true;
228
324
  } catch (error) {
229
325
  return false;
@@ -256,4 +352,120 @@ export class AdminHandler {
256
352
  name: 'adminIdentityGuard',
257
353
  }
258
354
  );
355
+
356
+ private async authenticateUser(optionsArg: {
357
+ username: string;
358
+ password: string;
359
+ authSource?: interfaces.requests.TAdminLoginAuthSource;
360
+ }): Promise<TAdminUser | null> {
361
+ if (await this.hasPersistentAdminAccount()) {
362
+ const store = this.getAccountStore();
363
+ const authService = new plugins.idpSdkServer.AccountAuthService({
364
+ store: store!,
365
+ idpClient: this.getIdpClient() as plugins.idpSdkServer.IdpGlobalServerClient | undefined,
366
+ });
367
+ const result = await authService.authenticate({
368
+ email: optionsArg.username,
369
+ password: optionsArg.password,
370
+ authSource: optionsArg.authSource || 'auto',
371
+ });
372
+ return result ? this.accountToUser(result.account) : null;
373
+ }
374
+
375
+ for (const [_, userData] of this.users) {
376
+ if (userData.username === optionsArg.username && userData.password === optionsArg.password) {
377
+ return userData;
378
+ }
379
+ }
380
+ return null;
381
+ }
382
+
383
+ private async resolveUser(userIdArg: string): Promise<TAdminUser | null> {
384
+ if (await this.hasPersistentAdminAccount()) {
385
+ const account = await this.getAccountStore()!.getAccountById(userIdArg);
386
+ if (!account || account.status !== 'active') {
387
+ return null;
388
+ }
389
+ return this.accountToUser(account);
390
+ }
391
+
392
+ return this.users.get(userIdArg) || null;
393
+ }
394
+
395
+ private async hasPersistentAdminAccount(): Promise<boolean> {
396
+ const store = this.getAccountStore();
397
+ return store ? store.hasActiveAdminAccount() : false;
398
+ }
399
+
400
+ private getAccountStore(): plugins.idpSdkServer.SmartdataAccountStore | null {
401
+ if (this.opsServerRef.dcRouterRef.options.dbConfig?.enabled === false) {
402
+ return null;
403
+ }
404
+ const dcRouterDb = this.opsServerRef.dcRouterRef.dcRouterDb;
405
+ if (!dcRouterDb?.isReady()) {
406
+ return null;
407
+ }
408
+ if (!this.accountStore) {
409
+ this.accountStore = new plugins.idpSdkServer.SmartdataAccountStore({
410
+ smartdataDb: dcRouterDb.getDb(),
411
+ });
412
+ }
413
+ return this.accountStore;
414
+ }
415
+
416
+ private getIdpClient(): Pick<plugins.idpSdkServer.IdpGlobalServerClient, 'loginWithEmailAndPassword' | 'stop'> | undefined {
417
+ const configuredClient = this.opsServerRef.dcRouterRef.options.adminAuth?.idpClient;
418
+ if (configuredClient) {
419
+ return configuredClient;
420
+ }
421
+
422
+ const baseUrl = this.opsServerRef.dcRouterRef.options.adminAuth?.idpGlobalUrl || process.env.DCROUTER_IDP_GLOBAL_URL;
423
+ if (!baseUrl) {
424
+ return undefined;
425
+ }
426
+
427
+ if (!this.idpClient) {
428
+ this.idpClient = new plugins.idpSdkServer.IdpGlobalServerClient({ baseUrl });
429
+ this.ownsIdpClient = true;
430
+ }
431
+ return this.idpClient;
432
+ }
433
+
434
+ private isIdpGlobalConfigured(): boolean {
435
+ return !!(
436
+ this.opsServerRef.dcRouterRef.options.adminAuth?.idpClient ||
437
+ this.opsServerRef.dcRouterRef.options.adminAuth?.idpGlobalUrl ||
438
+ process.env.DCROUTER_IDP_GLOBAL_URL
439
+ );
440
+ }
441
+
442
+ private accountToUser(accountArg: plugins.idpSdkServer.IIdpSdkAccount): TAdminUser {
443
+ return {
444
+ id: accountArg.id,
445
+ username: accountArg.email,
446
+ email: accountArg.email,
447
+ name: accountArg.name,
448
+ role: accountArg.role,
449
+ status: accountArg.status,
450
+ authSources: accountArg.authSources,
451
+ };
452
+ }
453
+
454
+ private async createIdentityForUser(userArg: TAdminUser): Promise<interfaces.data.IIdentity> {
455
+ const expiresAtTimestamp = Date.now() + 3600 * 1000 * 24; // 24 hours
456
+ const jwt = await this.smartjwtInstance.createJWT({
457
+ userId: userArg.id,
458
+ status: 'loggedIn',
459
+ expiresAt: expiresAtTimestamp,
460
+ });
461
+
462
+ return {
463
+ jwt,
464
+ userId: userArg.id,
465
+ name: userArg.name || userArg.username,
466
+ expiresAt: expiresAtTimestamp,
467
+ role: userArg.role,
468
+ type: 'user',
469
+ };
470
+ }
259
471
  }
@@ -88,6 +88,8 @@ export class TargetProfileHandler {
88
88
  routeRefs: dataArg.routeRefs,
89
89
  createdBy: userId,
90
90
  });
91
+ await this.opsServerRef.dcRouterRef.routeConfigManager?.applyRoutes();
92
+ await this.opsServerRef.dcRouterRef.vpnManager?.refreshAllClientSecurity();
91
93
  return { success: true, id };
92
94
  },
93
95
  ),
@@ -21,7 +21,7 @@ export class UsersHandler {
21
21
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListUsers>(
22
22
  'listUsers',
23
23
  async (_dataArg) => {
24
- const users = this.opsServerRef.adminHandler.listUsers();
24
+ const users = await this.opsServerRef.adminHandler.listUsers();
25
25
  return { users };
26
26
  },
27
27
  ),
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';
package/ts/readme.md CHANGED
@@ -91,7 +91,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
91
91
 
92
92
  ### Company Information
93
93
 
94
- Task Venture Capital GmbH
94
+ Task Venture Capital GmbH
95
95
  Registered at District Court Bremen HRB 35230 HB, Germany
96
96
 
97
97
  For any legal inquiries or further information, please contact us via email at hello@task.vc.
@@ -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;
@@ -27,7 +27,7 @@ const client = new DcRouterApiClient({
27
27
  baseUrl: 'https://dcrouter.example.com',
28
28
  });
29
29
 
30
- await client.login('admin', 'admin');
30
+ await client.login('admin@example.com', 'strong-password');
31
31
 
32
32
  const { routes, warnings } = await client.routes.list();
33
33
  console.log(routes.length, warnings.length);
@@ -43,13 +43,13 @@ await route.toggle(true);
43
43
 
44
44
  ## Authentication
45
45
 
46
- The client supports session login and API-token authentication.
46
+ The client supports persisted-admin session login and API-token authentication. Initial admin creation is a bootstrap flow exposed by the Ops dashboard and raw TypedRequest contracts; after a persisted admin exists, use that account with `login()`.
47
47
 
48
48
  ```typescript
49
49
  const sessionClient = new DcRouterApiClient({
50
50
  baseUrl: 'https://dcrouter.example.com',
51
51
  });
52
- await sessionClient.login('admin', 'admin');
52
+ await sessionClient.login('admin@example.com', 'strong-password');
53
53
 
54
54
  const tokenClient = new DcRouterApiClient({
55
55
  baseUrl: 'https://dcrouter.example.com',
@@ -153,7 +153,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
153
153
 
154
154
  ### Company Information
155
155
 
156
- Task Venture Capital GmbH
156
+ Task Venture Capital GmbH
157
157
  Registered at District Court Bremen HRB 35230 HB, Germany
158
158
 
159
159
  For any legal inquiries or further information, please contact us via email at hello@task.vc.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.28.0',
6
+ version: '13.30.0',
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;
@@ -312,7 +314,11 @@ export const routeManagementStatePart = await appState.getStatePart<IRouteManage
312
314
  export interface IUser {
313
315
  id: string;
314
316
  username: string;
317
+ email?: string;
318
+ name?: string;
315
319
  role: string;
320
+ status?: 'active' | 'disabled';
321
+ authSources?: Array<'local' | 'idp.global'>;
316
322
  }
317
323
 
318
324
  export interface IUsersState {
@@ -351,6 +357,7 @@ const getActionContext = (): IActionContext => {
351
357
  export const loginAction = loginStatePart.createAction<{
352
358
  username: string;
353
359
  password: string;
360
+ authSource?: interfaces.requests.TAdminLoginAuthSource;
354
361
  }>(async (statePartArg, dataArg): Promise<ILoginState> => {
355
362
  const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
356
363
  interfaces.requests.IReq_AdminLoginWithUsernameAndPassword
@@ -360,6 +367,7 @@ export const loginAction = loginStatePart.createAction<{
360
367
  const response = await typedRequest.fire({
361
368
  username: dataArg.username,
362
369
  password: dataArg.password,
370
+ authSource: dataArg.authSource,
363
371
  });
364
372
 
365
373
  if (response.identity) {
@@ -375,6 +383,47 @@ export const loginAction = loginStatePart.createAction<{
375
383
  }
376
384
  });
377
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
+
378
427
  // Logout Action — always clears state, even if identity is expired/missing
379
428
  export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
380
429
  const context = getActionContext();