@serve.zone/dcrouter 13.7.1 → 13.9.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 (71) hide show
  1. package/dist_serve/bundle.js +1763 -1519
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/acme/index.d.ts +1 -0
  4. package/dist_ts/acme/index.js +2 -0
  5. package/dist_ts/acme/manager.acme-config.d.ts +48 -0
  6. package/dist_ts/acme/manager.acme-config.js +156 -0
  7. package/dist_ts/classes.dcrouter.d.ts +2 -0
  8. package/dist_ts/classes.dcrouter.js +60 -21
  9. package/dist_ts/db/documents/classes.acme-config.doc.d.ts +22 -0
  10. package/dist_ts/db/documents/classes.acme-config.doc.js +121 -0
  11. package/dist_ts/db/documents/index.d.ts +1 -0
  12. package/dist_ts/db/documents/index.js +3 -1
  13. package/dist_ts/dns/manager.dns.d.ts +17 -15
  14. package/dist_ts/dns/manager.dns.js +33 -27
  15. package/dist_ts/dns/providers/factory.js +10 -1
  16. package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
  17. package/dist_ts/opsserver/classes.opsserver.js +3 -1
  18. package/dist_ts/opsserver/handlers/acme-config.handler.d.ts +16 -0
  19. package/dist_ts/opsserver/handlers/acme-config.handler.js +77 -0
  20. package/dist_ts/opsserver/handlers/dns-provider.handler.js +42 -5
  21. package/dist_ts/opsserver/handlers/domain.handler.js +3 -3
  22. package/dist_ts/opsserver/handlers/index.d.ts +1 -0
  23. package/dist_ts/opsserver/handlers/index.js +2 -1
  24. package/dist_ts_interfaces/data/acme-config.d.ts +25 -0
  25. package/dist_ts_interfaces/data/acme-config.js +2 -0
  26. package/dist_ts_interfaces/data/dns-provider.d.ts +28 -4
  27. package/dist_ts_interfaces/data/dns-provider.js +15 -1
  28. package/dist_ts_interfaces/data/dns-record.d.ts +9 -7
  29. package/dist_ts_interfaces/data/domain.d.ts +8 -7
  30. package/dist_ts_interfaces/data/index.d.ts +1 -0
  31. package/dist_ts_interfaces/data/index.js +2 -1
  32. package/dist_ts_interfaces/data/route-management.d.ts +1 -1
  33. package/dist_ts_interfaces/requests/acme-config.d.ts +42 -0
  34. package/dist_ts_interfaces/requests/acme-config.js +2 -0
  35. package/dist_ts_interfaces/requests/dns-records.d.ts +1 -1
  36. package/dist_ts_interfaces/requests/domains.d.ts +3 -3
  37. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  38. package/dist_ts_interfaces/requests/index.js +2 -1
  39. package/dist_ts_migrations/index.js +17 -1
  40. package/dist_ts_web/00_commitinfo_data.js +1 -1
  41. package/dist_ts_web/appstate.d.ts +16 -1
  42. package/dist_ts_web/appstate.js +61 -2
  43. package/dist_ts_web/elements/domains/dns-provider-form.d.ts +4 -2
  44. package/dist_ts_web/elements/domains/dns-provider-form.js +11 -5
  45. package/dist_ts_web/elements/domains/ops-view-certificates.d.ts +3 -0
  46. package/dist_ts_web/elements/domains/ops-view-certificates.js +208 -4
  47. package/dist_ts_web/elements/domains/ops-view-dns.js +3 -3
  48. package/dist_ts_web/elements/domains/ops-view-domains.d.ts +1 -1
  49. package/dist_ts_web/elements/domains/ops-view-domains.js +10 -10
  50. package/dist_ts_web/elements/domains/ops-view-providers.js +19 -5
  51. package/package.json +3 -3
  52. package/ts/00_commitinfo_data.ts +1 -1
  53. package/ts/acme/index.ts +1 -0
  54. package/ts/acme/manager.acme-config.ts +182 -0
  55. package/ts/classes.dcrouter.ts +74 -26
  56. package/ts/db/documents/classes.acme-config.doc.ts +49 -0
  57. package/ts/db/documents/index.ts +3 -0
  58. package/ts/dns/manager.dns.ts +38 -27
  59. package/ts/dns/providers/factory.ts +11 -0
  60. package/ts/opsserver/classes.opsserver.ts +2 -0
  61. package/ts/opsserver/handlers/acme-config.handler.ts +94 -0
  62. package/ts/opsserver/handlers/dns-provider.handler.ts +41 -3
  63. package/ts/opsserver/handlers/domain.handler.ts +2 -2
  64. package/ts/opsserver/handlers/index.ts +2 -1
  65. package/ts_web/00_commitinfo_data.ts +1 -1
  66. package/ts_web/appstate.ts +89 -1
  67. package/ts_web/elements/domains/dns-provider-form.ts +12 -4
  68. package/ts_web/elements/domains/ops-view-certificates.ts +205 -2
  69. package/ts_web/elements/domains/ops-view-dns.ts +2 -2
  70. package/ts_web/elements/domains/ops-view-domains.ts +9 -9
  71. package/ts_web/elements/domains/ops-view-providers.ts +18 -4
@@ -25,9 +25,9 @@ import type {
25
25
  * Responsibilities:
26
26
  * - Load Domain/DnsRecord docs from the DB on start
27
27
  * - First-boot seeding from legacy constructor config (dnsScopes/dnsRecords/dnsNsDomains)
28
- * - Register manual-domain records with smartdns.DnsServer at startup
29
- * - Provide CRUD methods used by OpsServer handlers (manual domains hit smartdns,
30
- * provider domains hit the provider API)
28
+ * - Register dcrouter-hosted domain records with smartdns.DnsServer at startup
29
+ * - Provide CRUD methods used by OpsServer handlers (dcrouter-hosted domains hit
30
+ * smartdns, provider domains hit the provider API)
31
31
  * - Expose a provider lookup used by the ACME DNS-01 wiring in setupSmartProxy()
32
32
  *
33
33
  * Provider-managed domains are NEVER served from the embedded DnsServer — the
@@ -69,12 +69,12 @@ export class DnsManager {
69
69
 
70
70
  /**
71
71
  * Wire the embedded DnsServer instance after it has been created by
72
- * DcRouter.setupDnsWithSocketHandler(). After this, manual records loaded
73
- * from the DB are registered with the server.
72
+ * DcRouter.setupDnsWithSocketHandler(). After this, local records on
73
+ * dcrouter-hosted domains loaded from the DB are registered with the server.
74
74
  */
75
75
  public async attachDnsServer(dnsServer: plugins.smartdns.dnsServerMod.DnsServer): Promise<void> {
76
76
  this.dnsServer = dnsServer;
77
- await this.applyManualDomainsToDnsServer();
77
+ await this.applyDcrouterDomainsToDnsServer();
78
78
  }
79
79
 
80
80
  // ==========================================================================
@@ -83,7 +83,8 @@ export class DnsManager {
83
83
 
84
84
  /**
85
85
  * If no DomainDocs exist yet but the constructor has legacy DNS fields,
86
- * seed them as `source: 'manual'` records. On subsequent boots (DB has
86
+ * seed them as dcrouter-hosted (`domain.source: 'dcrouter'`) zones with
87
+ * local (`record.source: 'local'`) records. On subsequent boots (DB has
87
88
  * entries), constructor config is ignored with a warning.
88
89
  */
89
90
  private async seedFromConstructorConfigIfEmpty(): Promise<void> {
@@ -117,7 +118,7 @@ export class DnsManager {
117
118
  const domain = new DomainDoc();
118
119
  domain.id = plugins.uuid.v4();
119
120
  domain.name = scope.toLowerCase();
120
- domain.source = 'manual';
121
+ domain.source = 'dcrouter';
121
122
  domain.authoritative = true;
122
123
  domain.createdAt = now;
123
124
  domain.updatedAt = now;
@@ -144,7 +145,7 @@ export class DnsManager {
144
145
  record.type = rec.type as TDnsRecordType;
145
146
  record.value = rec.value;
146
147
  record.ttl = rec.ttl ?? 300;
147
- record.source = 'manual';
148
+ record.source = 'local';
148
149
  record.createdAt = now;
149
150
  record.updatedAt = now;
150
151
  record.createdBy = 'seed';
@@ -174,28 +175,31 @@ export class DnsManager {
174
175
  }
175
176
 
176
177
  // ==========================================================================
177
- // Manual-domain DnsServer wiring
178
+ // DcRouter-hosted domain DnsServer wiring
178
179
  // ==========================================================================
179
180
 
180
181
  /**
181
- * Register all manual-domain records from the DB with the embedded DnsServer.
182
- * Called once after attachDnsServer().
182
+ * Register all records from dcrouter-hosted domains in the DB with the
183
+ * embedded DnsServer. Called once after attachDnsServer().
183
184
  */
184
- private async applyManualDomainsToDnsServer(): Promise<void> {
185
+ private async applyDcrouterDomainsToDnsServer(): Promise<void> {
185
186
  if (!this.dnsServer) {
186
187
  return;
187
188
  }
188
189
  const allDomains = await DomainDoc.findAll();
189
- const manualDomains = allDomains.filter((d) => d.source === 'manual');
190
+ const dcrouterDomains = allDomains.filter((d) => d.source === 'dcrouter');
190
191
  let registered = 0;
191
- for (const domain of manualDomains) {
192
+ for (const domain of dcrouterDomains) {
192
193
  const records = await DnsRecordDoc.findByDomainId(domain.id);
193
194
  for (const rec of records) {
194
195
  this.registerRecordWithDnsServer(rec);
195
196
  registered++;
196
197
  }
197
198
  }
198
- logger.log('info', `DnsManager: registered ${registered} manual DNS record(s) from DB`);
199
+ logger.log(
200
+ 'info',
201
+ `DnsManager: registered ${registered} dcrouter-hosted DNS record(s) from DB`,
202
+ );
199
203
  }
200
204
 
201
205
  /**
@@ -381,6 +385,12 @@ export class DnsManager {
381
385
  credentials: TDnsProviderCredentials;
382
386
  createdBy: string;
383
387
  }): Promise<string> {
388
+ if (args.type === 'dcrouter') {
389
+ throw new Error(
390
+ 'createProvider: cannot create a DnsProviderDoc with type "dcrouter" — ' +
391
+ 'that type is reserved for the built-in pseudo-provider surfaced at read time.',
392
+ );
393
+ }
384
394
  const now = Date.now();
385
395
  const doc = new DnsProviderDoc();
386
396
  doc.id = plugins.uuid.v4();
@@ -473,10 +483,10 @@ export class DnsManager {
473
483
  }
474
484
 
475
485
  /**
476
- * Create a manual (authoritative) domain. dcrouter will serve DNS records
477
- * for this domain via the embedded smartdns.DnsServer.
486
+ * Create a dcrouter-hosted (authoritative) domain. dcrouter will serve
487
+ * DNS records for this domain via the embedded smartdns.DnsServer.
478
488
  */
479
- public async createManualDomain(args: {
489
+ public async createDcrouterDomain(args: {
480
490
  name: string;
481
491
  description?: string;
482
492
  createdBy: string;
@@ -485,7 +495,7 @@ export class DnsManager {
485
495
  const doc = new DomainDoc();
486
496
  doc.id = plugins.uuid.v4();
487
497
  doc.name = args.name.toLowerCase();
488
- doc.source = 'manual';
498
+ doc.source = 'dcrouter';
489
499
  doc.authoritative = true;
490
500
  doc.description = args.description;
491
501
  doc.createdAt = now;
@@ -571,10 +581,11 @@ export class DnsManager {
571
581
  /**
572
582
  * Delete a domain and all of its DNS records. For provider domains, only
573
583
  * removes the local mirror — does NOT touch the provider.
574
- * For manual domains, also unregisters records from the embedded DnsServer.
584
+ * For dcrouter-hosted domains, also unregisters records from the embedded
585
+ * DnsServer.
575
586
  *
576
587
  * Note: smartdns has no public unregister-by-name API in the version pinned
577
- * here, so manual record deletes only take effect after a restart. The DB
588
+ * here, so local record deletes only take effect after a restart. The DB
578
589
  * is the source of truth and the next start will not register the deleted
579
590
  * record.
580
591
  */
@@ -652,7 +663,7 @@ export class DnsManager {
652
663
  doc.value = args.value;
653
664
  doc.ttl = args.ttl ?? 300;
654
665
  if (args.proxied !== undefined) doc.proxied = args.proxied;
655
- doc.source = 'manual';
666
+ doc.source = 'local';
656
667
  doc.createdAt = now;
657
668
  doc.updatedAt = now;
658
669
  doc.createdBy = args.createdBy;
@@ -678,7 +689,7 @@ export class DnsManager {
678
689
  return { success: false, message: `Provider rejected record: ${(err as Error).message}` };
679
690
  }
680
691
  } else {
681
- // Manual / authoritative — register with embedded DnsServer immediately
692
+ // dcrouter-hosted / authoritative — register with embedded DnsServer immediately
682
693
  this.registerRecordWithDnsServer(doc);
683
694
  }
684
695
 
@@ -722,7 +733,7 @@ export class DnsManager {
722
733
  return { success: false, message: `Provider rejected update: ${(err as Error).message}` };
723
734
  }
724
735
  } else {
725
- // Re-register the manual record so the new closure picks up the updated fields
736
+ // Re-register the local record so the new closure picks up the updated fields
726
737
  this.registerRecordWithDnsServer(doc);
727
738
  }
728
739
 
@@ -748,7 +759,7 @@ export class DnsManager {
748
759
  }
749
760
  }
750
761
  }
751
- // For manual records: smartdns has no unregister API in the pinned version,
762
+ // For local records: smartdns has no unregister API in the pinned version,
752
763
  // so the record stays served until the next restart. The DB delete still
753
764
  // takes effect — on restart, the record will not be re-registered.
754
765
 
@@ -807,7 +818,7 @@ export class DnsManager {
807
818
  public toPublicDomain(doc: DomainDoc): {
808
819
  id: string;
809
820
  name: string;
810
- source: 'manual' | 'provider';
821
+ source: 'dcrouter' | 'provider';
811
822
  providerId?: string;
812
823
  authoritative: boolean;
813
824
  nameservers?: string[];
@@ -38,6 +38,17 @@ export function createDnsProvider(
38
38
  }
39
39
  return new CloudflareDnsProvider(credentials.apiToken);
40
40
  }
41
+ case 'dcrouter': {
42
+ // The built-in DcRouter pseudo-provider has no runtime client — dcrouter
43
+ // itself serves the records via the embedded smartdns.DnsServer. This
44
+ // case exists only to satisfy the exhaustive switch; it should never
45
+ // actually run because the handler layer rejects any CRUD that would
46
+ // result in a DnsProviderDoc with type: 'dcrouter'.
47
+ throw new Error(
48
+ `createDnsProvider: 'dcrouter' is a built-in pseudo-provider — no runtime client exists. ` +
49
+ `This call indicates a DnsProviderDoc with type: 'dcrouter' was persisted, which should never happen.`,
50
+ );
51
+ }
41
52
  default: {
42
53
  // If you see a TypeScript error here after extending TDnsProviderType,
43
54
  // add a `case` for the new type above. The `never` enforces exhaustiveness.
@@ -36,6 +36,7 @@ export class OpsServer {
36
36
  private dnsProviderHandler!: handlers.DnsProviderHandler;
37
37
  private domainHandler!: handlers.DomainHandler;
38
38
  private dnsRecordHandler!: handlers.DnsRecordHandler;
39
+ private acmeConfigHandler!: handlers.AcmeConfigHandler;
39
40
 
40
41
  constructor(dcRouterRefArg: DcRouter) {
41
42
  this.dcRouterRef = dcRouterRefArg;
@@ -102,6 +103,7 @@ export class OpsServer {
102
103
  this.dnsProviderHandler = new handlers.DnsProviderHandler(this);
103
104
  this.domainHandler = new handlers.DomainHandler(this);
104
105
  this.dnsRecordHandler = new handlers.DnsRecordHandler(this);
106
+ this.acmeConfigHandler = new handlers.AcmeConfigHandler(this);
105
107
 
106
108
  console.log('✅ OpsServer TypedRequest handlers initialized');
107
109
  }
@@ -0,0 +1,94 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import type { OpsServer } from '../classes.opsserver.js';
3
+ import * as interfaces from '../../../ts_interfaces/index.js';
4
+
5
+ /**
6
+ * CRUD handler for the singleton `AcmeConfigDoc`.
7
+ *
8
+ * Auth: same dual-mode pattern as other handlers — admin JWT or API token
9
+ * with `acme-config:read` / `acme-config:write` scope.
10
+ */
11
+ export class AcmeConfigHandler {
12
+ public typedrouter = new plugins.typedrequest.TypedRouter();
13
+
14
+ constructor(private opsServerRef: OpsServer) {
15
+ this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
16
+ this.registerHandlers();
17
+ }
18
+
19
+ private async requireAuth(
20
+ request: { identity?: interfaces.data.IIdentity; apiToken?: string },
21
+ requiredScope?: interfaces.data.TApiTokenScope,
22
+ ): Promise<string> {
23
+ if (request.identity?.jwt) {
24
+ try {
25
+ const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({
26
+ identity: request.identity,
27
+ });
28
+ if (isAdmin) return request.identity.userId;
29
+ } catch { /* fall through */ }
30
+ }
31
+
32
+ if (request.apiToken) {
33
+ const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
34
+ if (tokenManager) {
35
+ const token = await tokenManager.validateToken(request.apiToken);
36
+ if (token) {
37
+ if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
38
+ return token.createdBy;
39
+ }
40
+ throw new plugins.typedrequest.TypedResponseError('insufficient scope');
41
+ }
42
+ }
43
+ }
44
+
45
+ throw new plugins.typedrequest.TypedResponseError('unauthorized');
46
+ }
47
+
48
+ private registerHandlers(): void {
49
+ // Get current ACME config
50
+ this.typedrouter.addTypedHandler(
51
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAcmeConfig>(
52
+ 'getAcmeConfig',
53
+ async (dataArg) => {
54
+ await this.requireAuth(dataArg, 'acme-config:read');
55
+ const mgr = this.opsServerRef.dcRouterRef.acmeConfigManager;
56
+ if (!mgr) return { config: null };
57
+ return { config: mgr.getConfig() };
58
+ },
59
+ ),
60
+ );
61
+
62
+ // Update (upsert) the ACME config
63
+ this.typedrouter.addTypedHandler(
64
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateAcmeConfig>(
65
+ 'updateAcmeConfig',
66
+ async (dataArg) => {
67
+ const userId = await this.requireAuth(dataArg, 'acme-config:write');
68
+ const mgr = this.opsServerRef.dcRouterRef.acmeConfigManager;
69
+ if (!mgr) {
70
+ return {
71
+ success: false,
72
+ message: 'AcmeConfigManager not initialized (DB disabled?)',
73
+ };
74
+ }
75
+ try {
76
+ const updated = await mgr.updateConfig(
77
+ {
78
+ accountEmail: dataArg.accountEmail,
79
+ enabled: dataArg.enabled,
80
+ useProduction: dataArg.useProduction,
81
+ autoRenew: dataArg.autoRenew,
82
+ renewThresholdDays: dataArg.renewThresholdDays,
83
+ },
84
+ userId,
85
+ );
86
+ return { success: true, config: updated };
87
+ } catch (err: unknown) {
88
+ return { success: false, message: (err as Error).message };
89
+ }
90
+ },
91
+ ),
92
+ );
93
+ }
94
+ }
@@ -46,15 +46,28 @@ export class DnsProviderHandler {
46
46
  }
47
47
 
48
48
  private registerHandlers(): void {
49
- // Get all providers
49
+ // Get all providers — prepends the built-in DcRouter pseudo-provider
50
+ // so operators see a uniform "who serves this?" list that includes the
51
+ // authoritative dcrouter alongside external accounts.
50
52
  this.typedrouter.addTypedHandler(
51
53
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsProviders>(
52
54
  'getDnsProviders',
53
55
  async (dataArg) => {
54
56
  await this.requireAuth(dataArg, 'dns-providers:read');
55
57
  const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
56
- if (!dnsManager) return { providers: [] };
57
- return { providers: await dnsManager.listProviders() };
58
+ const synthetic: interfaces.data.IDnsProviderPublic = {
59
+ id: interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID,
60
+ name: 'DcRouter',
61
+ type: 'dcrouter',
62
+ status: 'ok',
63
+ createdAt: 0,
64
+ updatedAt: 0,
65
+ createdBy: 'system',
66
+ hasCredentials: false,
67
+ builtIn: true,
68
+ };
69
+ const real = dnsManager ? await dnsManager.listProviders() : [];
70
+ return { providers: [synthetic, ...real] };
58
71
  },
59
72
  ),
60
73
  );
@@ -78,6 +91,12 @@ export class DnsProviderHandler {
78
91
  'createDnsProvider',
79
92
  async (dataArg) => {
80
93
  const userId = await this.requireAuth(dataArg, 'dns-providers:write');
94
+ if (dataArg.type === 'dcrouter') {
95
+ return {
96
+ success: false,
97
+ message: 'cannot create built-in provider',
98
+ };
99
+ }
81
100
  const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
82
101
  if (!dnsManager) {
83
102
  return { success: false, message: 'DnsManager not initialized (DB disabled?)' };
@@ -99,6 +118,9 @@ export class DnsProviderHandler {
99
118
  'updateDnsProvider',
100
119
  async (dataArg) => {
101
120
  await this.requireAuth(dataArg, 'dns-providers:write');
121
+ if (dataArg.id === interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID) {
122
+ return { success: false, message: 'cannot edit built-in provider' };
123
+ }
102
124
  const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
103
125
  if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
104
126
  const ok = await dnsManager.updateProvider(dataArg.id, {
@@ -116,6 +138,9 @@ export class DnsProviderHandler {
116
138
  'deleteDnsProvider',
117
139
  async (dataArg) => {
118
140
  await this.requireAuth(dataArg, 'dns-providers:write');
141
+ if (dataArg.id === interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID) {
142
+ return { success: false, message: 'cannot delete built-in provider' };
143
+ }
119
144
  const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
120
145
  if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
121
146
  return await dnsManager.deleteProvider(dataArg.id, dataArg.force ?? false);
@@ -129,6 +154,13 @@ export class DnsProviderHandler {
129
154
  'testDnsProvider',
130
155
  async (dataArg) => {
131
156
  await this.requireAuth(dataArg, 'dns-providers:read');
157
+ if (dataArg.id === interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID) {
158
+ return {
159
+ ok: false,
160
+ error: 'built-in provider has no external connection to test',
161
+ testedAt: Date.now(),
162
+ };
163
+ }
132
164
  const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
133
165
  if (!dnsManager) {
134
166
  return { ok: false, error: 'DnsManager not initialized', testedAt: Date.now() };
@@ -144,6 +176,12 @@ export class DnsProviderHandler {
144
176
  'listProviderDomains',
145
177
  async (dataArg) => {
146
178
  await this.requireAuth(dataArg, 'dns-providers:read');
179
+ if (dataArg.providerId === interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID) {
180
+ return {
181
+ success: false,
182
+ message: 'built-in provider has no external domain listing — use "Add DcRouter Domain" instead',
183
+ };
184
+ }
147
185
  const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
148
186
  if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
149
187
  try {
@@ -71,7 +71,7 @@ export class DomainHandler {
71
71
  ),
72
72
  );
73
73
 
74
- // Create manual domain
74
+ // Create dcrouter-hosted domain
75
75
  this.typedrouter.addTypedHandler(
76
76
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateDomain>(
77
77
  'createDomain',
@@ -80,7 +80,7 @@ export class DomainHandler {
80
80
  const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
81
81
  if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
82
82
  try {
83
- const id = await dnsManager.createManualDomain({
83
+ const id = await dnsManager.createDcrouterDomain({
84
84
  name: dataArg.name,
85
85
  description: dataArg.description,
86
86
  createdBy: userId,
@@ -16,4 +16,5 @@ export * from './network-target.handler.js';
16
16
  export * from './users.handler.js';
17
17
  export * from './dns-provider.handler.js';
18
18
  export * from './domain.handler.js';
19
- export * from './dns-record.handler.js';
19
+ export * from './dns-record.handler.js';
20
+ export * from './acme-config.handler.js';
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.7.1',
6
+ version: '13.9.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -197,6 +197,28 @@ export const certificateStatePart = await appState.getStatePart<ICertificateStat
197
197
  'soft'
198
198
  );
199
199
 
200
+ // ============================================================================
201
+ // ACME Config State (DB-backed singleton, managed via Domains > Certificates)
202
+ // ============================================================================
203
+
204
+ export interface IAcmeConfigState {
205
+ config: interfaces.data.IAcmeConfig | null;
206
+ isLoading: boolean;
207
+ error: string | null;
208
+ lastUpdated: number;
209
+ }
210
+
211
+ export const acmeConfigStatePart = await appState.getStatePart<IAcmeConfigState>(
212
+ 'acmeConfig',
213
+ {
214
+ config: null,
215
+ isLoading: false,
216
+ error: null,
217
+ lastUpdated: 0,
218
+ },
219
+ 'soft',
220
+ );
221
+
200
222
  // ============================================================================
201
223
  // Remote Ingress State
202
224
  // ============================================================================
@@ -1771,7 +1793,7 @@ export async function fetchProviderDomains(
1771
1793
  return await request.fire({ identity: context.identity, providerId });
1772
1794
  }
1773
1795
 
1774
- export const createManualDomainAction = domainsStatePart.createAction<{
1796
+ export const createDcrouterDomainAction = domainsStatePart.createAction<{
1775
1797
  name: string;
1776
1798
  description?: string;
1777
1799
  }>(async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {
@@ -1953,6 +1975,72 @@ export const deleteDnsRecordAction = domainsStatePart.createAction<{ id: string;
1953
1975
  },
1954
1976
  );
1955
1977
 
1978
+ // ============================================================================
1979
+ // ACME Config Actions
1980
+ // ============================================================================
1981
+
1982
+ export const fetchAcmeConfigAction = acmeConfigStatePart.createAction(
1983
+ async (statePartArg): Promise<IAcmeConfigState> => {
1984
+ const context = getActionContext();
1985
+ const currentState = statePartArg.getState()!;
1986
+ if (!context.identity) return currentState;
1987
+
1988
+ try {
1989
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1990
+ interfaces.requests.IReq_GetAcmeConfig
1991
+ >('/typedrequest', 'getAcmeConfig');
1992
+ const response = await request.fire({ identity: context.identity });
1993
+ return {
1994
+ config: response.config,
1995
+ isLoading: false,
1996
+ error: null,
1997
+ lastUpdated: Date.now(),
1998
+ };
1999
+ } catch (error: unknown) {
2000
+ return {
2001
+ ...currentState,
2002
+ isLoading: false,
2003
+ error: error instanceof Error ? error.message : 'Failed to fetch ACME config',
2004
+ };
2005
+ }
2006
+ },
2007
+ );
2008
+
2009
+ export const updateAcmeConfigAction = acmeConfigStatePart.createAction<{
2010
+ accountEmail?: string;
2011
+ enabled?: boolean;
2012
+ useProduction?: boolean;
2013
+ autoRenew?: boolean;
2014
+ renewThresholdDays?: number;
2015
+ }>(async (statePartArg, dataArg, actionContext): Promise<IAcmeConfigState> => {
2016
+ const context = getActionContext();
2017
+ try {
2018
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2019
+ interfaces.requests.IReq_UpdateAcmeConfig
2020
+ >('/typedrequest', 'updateAcmeConfig');
2021
+ const response = await request.fire({
2022
+ identity: context.identity!,
2023
+ accountEmail: dataArg.accountEmail,
2024
+ enabled: dataArg.enabled,
2025
+ useProduction: dataArg.useProduction,
2026
+ autoRenew: dataArg.autoRenew,
2027
+ renewThresholdDays: dataArg.renewThresholdDays,
2028
+ });
2029
+ if (!response.success) {
2030
+ return {
2031
+ ...statePartArg.getState()!,
2032
+ error: response.message || 'Failed to update ACME config',
2033
+ };
2034
+ }
2035
+ return await actionContext!.dispatch(fetchAcmeConfigAction, null);
2036
+ } catch (error: unknown) {
2037
+ return {
2038
+ ...statePartArg.getState()!,
2039
+ error: error instanceof Error ? error.message : 'Failed to update ACME config',
2040
+ };
2041
+ }
2042
+ });
2043
+
1956
2044
  // ============================================================================
1957
2045
  // Route Management Actions
1958
2046
  // ============================================================================
@@ -44,12 +44,15 @@ export class DnsProviderForm extends DeesElement {
44
44
  accessor providerName: string = '';
45
45
 
46
46
  /**
47
- * Currently selected provider type. Initialized to the first descriptor;
48
- * caller can override before mounting (e.g. for edit dialogs).
47
+ * Currently selected provider type. Initialized to the first user-creatable
48
+ * descriptor; caller can override before mounting (e.g. for edit dialogs).
49
+ * The built-in 'dcrouter' pseudo-provider is excluded from the picker —
50
+ * operators cannot create another one.
49
51
  */
50
52
  @state()
51
53
  accessor selectedType: interfaces.data.TDnsProviderType =
52
- interfaces.data.dnsProviderTypeDescriptors[0]?.type ?? 'cloudflare';
54
+ interfaces.data.dnsProviderTypeDescriptors.find((d) => d.type !== 'dcrouter')?.type ??
55
+ 'cloudflare';
53
56
 
54
57
  /** When true, hide the type picker — used in edit dialogs. */
55
58
  @property({ type: Boolean })
@@ -102,7 +105,12 @@ export class DnsProviderForm extends DeesElement {
102
105
  ];
103
106
 
104
107
  public render(): TemplateResult {
105
- const descriptors = interfaces.data.dnsProviderTypeDescriptors;
108
+ // Exclude the built-in 'dcrouter' pseudo-provider from the type picker —
109
+ // operators cannot create another one, it's surfaced at read time by the
110
+ // backend handler instead.
111
+ const descriptors = interfaces.data.dnsProviderTypeDescriptors.filter(
112
+ (d) => d.type !== 'dcrouter',
113
+ );
106
114
  const descriptor = interfaces.data.getDnsProviderTypeDescriptor(this.selectedType);
107
115
 
108
116
  return html`