@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
@@ -23,17 +23,25 @@ export class OpsViewCertificates extends DeesElement {
23
23
  @state()
24
24
  accessor certState: appstate.ICertificateState = appstate.certificateStatePart.getState()!;
25
25
 
26
+ @state()
27
+ accessor acmeState: appstate.IAcmeConfigState = appstate.acmeConfigStatePart.getState()!;
28
+
26
29
  constructor() {
27
30
  super();
28
- const sub = appstate.certificateStatePart.select().subscribe((newState) => {
31
+ const certSub = appstate.certificateStatePart.select().subscribe((newState) => {
29
32
  this.certState = newState;
30
33
  });
31
- this.rxSubscriptions.push(sub);
34
+ this.rxSubscriptions.push(certSub);
35
+ const acmeSub = appstate.acmeConfigStatePart.select().subscribe((newState) => {
36
+ this.acmeState = newState;
37
+ });
38
+ this.rxSubscriptions.push(acmeSub);
32
39
  }
33
40
 
34
41
  async connectedCallback() {
35
42
  await super.connectedCallback();
36
43
  await appstate.certificateStatePart.dispatchAction(appstate.fetchCertificateOverviewAction, null);
44
+ await appstate.acmeConfigStatePart.dispatchAction(appstate.fetchAcmeConfigAction, null);
37
45
  }
38
46
 
39
47
  public static styles = [
@@ -46,6 +54,62 @@ export class OpsViewCertificates extends DeesElement {
46
54
  gap: 24px;
47
55
  }
48
56
 
57
+ .acmeCard {
58
+ padding: 16px 20px;
59
+ background: ${cssManager.bdTheme('#f9fafb', '#111827')};
60
+ border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
61
+ border-radius: 8px;
62
+ }
63
+
64
+ .acmeCard.acmeCardEmpty {
65
+ background: ${cssManager.bdTheme('#fffbeb', '#1c1917')};
66
+ border-color: ${cssManager.bdTheme('#fde68a', '#78350f')};
67
+ }
68
+
69
+ .acmeCardHeader {
70
+ display: flex;
71
+ justify-content: space-between;
72
+ align-items: center;
73
+ margin-bottom: 12px;
74
+ }
75
+
76
+ .acmeCardTitle {
77
+ font-size: 14px;
78
+ font-weight: 600;
79
+ color: ${cssManager.bdTheme('#111827', '#f3f4f6')};
80
+ }
81
+
82
+ .acmeGrid {
83
+ display: grid;
84
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
85
+ gap: 12px 24px;
86
+ }
87
+
88
+ .acmeField {
89
+ display: flex;
90
+ flex-direction: column;
91
+ gap: 2px;
92
+ }
93
+
94
+ .acmeLabel {
95
+ font-size: 11px;
96
+ text-transform: uppercase;
97
+ letter-spacing: 0.03em;
98
+ color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
99
+ }
100
+
101
+ .acmeValue {
102
+ font-size: 13px;
103
+ color: ${cssManager.bdTheme('#111827', '#f3f4f6')};
104
+ }
105
+
106
+ .acmeEmptyHint {
107
+ margin: 0;
108
+ font-size: 13px;
109
+ line-height: 1.5;
110
+ color: ${cssManager.bdTheme('#78350f', '#fde68a')};
111
+ }
112
+
49
113
  .statusBadge {
50
114
  display: inline-flex;
51
115
  align-items: center;
@@ -162,12 +226,151 @@ export class OpsViewCertificates extends DeesElement {
162
226
  <dees-heading level="3">Certificates</dees-heading>
163
227
 
164
228
  <div class="certificatesContainer">
229
+ ${this.renderAcmeSettingsCard()}
165
230
  ${this.renderStatsTiles(summary)}
166
231
  ${this.renderCertificateTable()}
167
232
  </div>
168
233
  `;
169
234
  }
170
235
 
236
+ private renderAcmeSettingsCard(): TemplateResult {
237
+ const config = this.acmeState.config;
238
+
239
+ if (!config) {
240
+ return html`
241
+ <div class="acmeCard acmeCardEmpty">
242
+ <div class="acmeCardHeader">
243
+ <span class="acmeCardTitle">ACME Settings</span>
244
+ <dees-button
245
+ eventName="edit-acme"
246
+ @click=${() => this.showEditAcmeDialog()}
247
+ .type=${'highlighted'}
248
+ >Configure</dees-button>
249
+ </div>
250
+ <p class="acmeEmptyHint">
251
+ No ACME configuration yet. Click <strong>Configure</strong> to set up automated TLS
252
+ certificate issuance via Let's Encrypt. You'll also need at least one DNS provider
253
+ under <strong>Domains &gt; Providers</strong>.
254
+ </p>
255
+ </div>
256
+ `;
257
+ }
258
+
259
+ return html`
260
+ <div class="acmeCard">
261
+ <div class="acmeCardHeader">
262
+ <span class="acmeCardTitle">ACME Settings</span>
263
+ <dees-button eventName="edit-acme" @click=${() => this.showEditAcmeDialog()}>Edit</dees-button>
264
+ </div>
265
+ <div class="acmeGrid">
266
+ <div class="acmeField">
267
+ <span class="acmeLabel">Account email</span>
268
+ <span class="acmeValue">${config.accountEmail || '(not set)'}</span>
269
+ </div>
270
+ <div class="acmeField">
271
+ <span class="acmeLabel">Status</span>
272
+ <span class="acmeValue">
273
+ <span class="statusBadge ${config.enabled ? 'valid' : 'unknown'}">
274
+ ${config.enabled ? 'enabled' : 'disabled'}
275
+ </span>
276
+ </span>
277
+ </div>
278
+ <div class="acmeField">
279
+ <span class="acmeLabel">Mode</span>
280
+ <span class="acmeValue">
281
+ <span class="statusBadge ${config.useProduction ? 'valid' : 'provisioning'}">
282
+ ${config.useProduction ? 'production' : 'staging'}
283
+ </span>
284
+ </span>
285
+ </div>
286
+ <div class="acmeField">
287
+ <span class="acmeLabel">Auto-renew</span>
288
+ <span class="acmeValue">${config.autoRenew ? 'on' : 'off'}</span>
289
+ </div>
290
+ <div class="acmeField">
291
+ <span class="acmeLabel">Renewal threshold</span>
292
+ <span class="acmeValue">${config.renewThresholdDays} days</span>
293
+ </div>
294
+ </div>
295
+ </div>
296
+ `;
297
+ }
298
+
299
+ private async showEditAcmeDialog() {
300
+ const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
301
+ const current = this.acmeState.config;
302
+
303
+ DeesModal.createAndShow({
304
+ heading: current ? 'Edit ACME Settings' : 'Configure ACME',
305
+ content: html`
306
+ <dees-form>
307
+ <dees-input-text
308
+ .key=${'accountEmail'}
309
+ .label=${'Account email'}
310
+ .value=${current?.accountEmail ?? ''}
311
+ .required=${true}
312
+ ></dees-input-text>
313
+ <dees-input-checkbox
314
+ .key=${'enabled'}
315
+ .label=${'Enabled'}
316
+ .value=${current?.enabled ?? true}
317
+ ></dees-input-checkbox>
318
+ <dees-input-checkbox
319
+ .key=${'useProduction'}
320
+ .label=${"Use Let's Encrypt production (uncheck for staging)"}
321
+ .value=${current?.useProduction ?? true}
322
+ ></dees-input-checkbox>
323
+ <dees-input-checkbox
324
+ .key=${'autoRenew'}
325
+ .label=${'Auto-renew certificates'}
326
+ .value=${current?.autoRenew ?? true}
327
+ ></dees-input-checkbox>
328
+ <dees-input-text
329
+ .key=${'renewThresholdDays'}
330
+ .label=${'Renewal threshold (days)'}
331
+ .value=${String(current?.renewThresholdDays ?? 30)}
332
+ ></dees-input-text>
333
+ </dees-form>
334
+ <p style="margin-top: 12px; font-size: 12px; opacity: 0.7;">
335
+ Most fields take effect on the next dcrouter restart (SmartAcme is instantiated once at
336
+ startup). Changing the account email creates a new Let's Encrypt account — only do this
337
+ if you know what you're doing.
338
+ </p>
339
+ `,
340
+ menuOptions: [
341
+ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
342
+ {
343
+ name: 'Save',
344
+ action: async (modalArg: any) => {
345
+ const form = modalArg.shadowRoot
346
+ ?.querySelector('.content')
347
+ ?.querySelector('dees-form');
348
+ if (!form) return;
349
+ const data = await form.collectFormData();
350
+ const email = String(data.accountEmail ?? '').trim();
351
+ if (!email) {
352
+ DeesToast.show({
353
+ message: 'Account email is required',
354
+ type: 'warning',
355
+ duration: 2500,
356
+ });
357
+ return;
358
+ }
359
+ const threshold = parseInt(String(data.renewThresholdDays ?? '30'), 10);
360
+ await appstate.acmeConfigStatePart.dispatchAction(appstate.updateAcmeConfigAction, {
361
+ accountEmail: email,
362
+ enabled: Boolean(data.enabled),
363
+ useProduction: Boolean(data.useProduction),
364
+ autoRenew: Boolean(data.autoRenew),
365
+ renewThresholdDays: Number.isFinite(threshold) ? threshold : 30,
366
+ });
367
+ modalArg.destroy();
368
+ },
369
+ },
370
+ ],
371
+ });
372
+ }
373
+
171
374
  private renderStatsTiles(summary: appstate.ICertificateState['summary']): TemplateResult {
172
375
  const tiles: IStatsTile[] = [
173
376
  {
@@ -80,7 +80,7 @@ export class OpsViewDns extends DeesElement {
80
80
  font-weight: 500;
81
81
  }
82
82
 
83
- .sourceBadge.manual {
83
+ .sourceBadge.local {
84
84
  background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
85
85
  color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
86
86
  }
@@ -184,7 +184,7 @@ export class OpsViewDns extends DeesElement {
184
184
  private domainHint(domainId: string): string {
185
185
  const domain = this.domainsState.domains.find((d) => d.id === domainId);
186
186
  if (!domain) return '';
187
- if (domain.source === 'manual') {
187
+ if (domain.source === 'dcrouter') {
188
188
  return 'Records are served by dcrouter (authoritative).';
189
189
  }
190
190
  return 'Records are stored at the provider — changes here are pushed via the provider API.';
@@ -55,7 +55,7 @@ export class OpsViewDomains extends DeesElement {
55
55
  font-weight: 500;
56
56
  }
57
57
 
58
- .sourceBadge.manual {
58
+ .sourceBadge.dcrouter {
59
59
  background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
60
60
  color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
61
61
  }
@@ -76,7 +76,7 @@ export class OpsViewDomains extends DeesElement {
76
76
  <div class="domainsContainer">
77
77
  <dees-table
78
78
  .heading1=${'Domains'}
79
- .heading2=${'Domains under management — manual (authoritative) or imported from a provider'}
79
+ .heading2=${'Domains under management — served by dcrouter (authoritative) or imported from a provider'}
80
80
  .data=${domains}
81
81
  .showColumnFilters=${true}
82
82
  .displayFunction=${(d: interfaces.data.IDomain) => ({
@@ -90,11 +90,11 @@ export class OpsViewDomains extends DeesElement {
90
90
  })}
91
91
  .dataActions=${[
92
92
  {
93
- name: 'Add Manual Domain',
93
+ name: 'Add DcRouter Domain',
94
94
  iconName: 'lucide:plus',
95
95
  type: ['header' as const],
96
96
  actionFunc: async () => {
97
- await this.showCreateManualDialog();
97
+ await this.showCreateDcrouterDialog();
98
98
  },
99
99
  },
100
100
  {
@@ -168,17 +168,17 @@ export class OpsViewDomains extends DeesElement {
168
168
  d: interfaces.data.IDomain,
169
169
  providersById: Map<string, interfaces.data.IDnsProviderPublic>,
170
170
  ): TemplateResult {
171
- if (d.source === 'manual') {
172
- return html`<span class="sourceBadge manual">Manual</span>`;
171
+ if (d.source === 'dcrouter') {
172
+ return html`<span class="sourceBadge dcrouter">DcRouter</span>`;
173
173
  }
174
174
  const provider = d.providerId ? providersById.get(d.providerId) : undefined;
175
175
  return html`<span class="sourceBadge provider">${provider?.name || 'Provider'}</span>`;
176
176
  }
177
177
 
178
- private async showCreateManualDialog() {
178
+ private async showCreateDcrouterDialog() {
179
179
  const { DeesModal } = await import('@design.estate/dees-catalog');
180
180
  DeesModal.createAndShow({
181
- heading: 'Add Manual Domain',
181
+ heading: 'Add DcRouter Domain',
182
182
  content: html`
183
183
  <dees-form>
184
184
  <dees-input-text .key=${'name'} .label=${'FQDN (e.g. example.com)'} .required=${true}></dees-input-text>
@@ -199,7 +199,7 @@ export class OpsViewDomains extends DeesElement {
199
199
  ?.querySelector('dees-form');
200
200
  if (!form) return;
201
201
  const data = await form.collectFormData();
202
- await appstate.domainsStatePart.dispatchAction(appstate.createManualDomainAction, {
202
+ await appstate.domainsStatePart.dispatchAction(appstate.createDcrouterDomainAction, {
203
203
  name: String(data.name),
204
204
  description: data.description ? String(data.description) : undefined,
205
205
  });
@@ -71,6 +71,11 @@ export class OpsViewProviders extends DeesElement {
71
71
  background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
72
72
  color: ${cssManager.bdTheme('#4b5563', '#9ca3af')};
73
73
  }
74
+
75
+ .statusBadge.builtin {
76
+ background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
77
+ color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
78
+ }
74
79
  `,
75
80
  ];
76
81
 
@@ -82,15 +87,21 @@ export class OpsViewProviders extends DeesElement {
82
87
  <div class="providersContainer">
83
88
  <dees-table
84
89
  .heading1=${'Providers'}
85
- .heading2=${'External DNS provider accounts'}
90
+ .heading2=${'Built-in dcrouter + external DNS provider accounts'}
86
91
  .data=${providers}
87
92
  .showColumnFilters=${true}
88
93
  .displayFunction=${(p: interfaces.data.IDnsProviderPublic) => ({
89
94
  Name: p.name,
90
95
  Type: this.providerTypeLabel(p.type),
91
- Status: this.renderStatusBadge(p.status),
92
- 'Last Tested': p.lastTestedAt ? new Date(p.lastTestedAt).toLocaleString() : 'never',
93
- Error: p.lastError || '-',
96
+ Status: p.builtIn
97
+ ? html`<span class="statusBadge builtin">built-in</span>`
98
+ : this.renderStatusBadge(p.status),
99
+ 'Last Tested': p.builtIn
100
+ ? '—'
101
+ : p.lastTestedAt
102
+ ? new Date(p.lastTestedAt).toLocaleString()
103
+ : 'never',
104
+ Error: p.builtIn ? '—' : p.lastError || '-',
94
105
  })}
95
106
  .dataActions=${[
96
107
  {
@@ -116,6 +127,7 @@ export class OpsViewProviders extends DeesElement {
116
127
  name: 'Test Connection',
117
128
  iconName: 'lucide:plug',
118
129
  type: ['inRow', 'contextmenu'] as any,
130
+ actionRelevancyCheckFunc: (p: interfaces.data.IDnsProviderPublic) => !p.builtIn,
119
131
  actionFunc: async (actionData: any) => {
120
132
  const provider = actionData.item as interfaces.data.IDnsProviderPublic;
121
133
  await this.testProvider(provider);
@@ -125,6 +137,7 @@ export class OpsViewProviders extends DeesElement {
125
137
  name: 'Edit',
126
138
  iconName: 'lucide:pencil',
127
139
  type: ['inRow', 'contextmenu'] as any,
140
+ actionRelevancyCheckFunc: (p: interfaces.data.IDnsProviderPublic) => !p.builtIn,
128
141
  actionFunc: async (actionData: any) => {
129
142
  const provider = actionData.item as interfaces.data.IDnsProviderPublic;
130
143
  await this.showEditDialog(provider);
@@ -134,6 +147,7 @@ export class OpsViewProviders extends DeesElement {
134
147
  name: 'Delete',
135
148
  iconName: 'lucide:trash2',
136
149
  type: ['inRow', 'contextmenu'] as any,
150
+ actionRelevancyCheckFunc: (p: interfaces.data.IDnsProviderPublic) => !p.builtIn,
137
151
  actionFunc: async (actionData: any) => {
138
152
  const provider = actionData.item as interfaces.data.IDnsProviderPublic;
139
153
  await this.deleteProvider(provider);