@serve.zone/dcrouter 5.2.0 → 5.4.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 (36) hide show
  1. package/dist_serve/bundle.js +2444 -2300
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +12 -0
  4. package/dist_ts/classes.dcrouter.js +53 -6
  5. package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
  6. package/dist_ts/opsserver/classes.opsserver.js +3 -1
  7. package/dist_ts/opsserver/handlers/certificate.handler.d.ts +11 -0
  8. package/dist_ts/opsserver/handlers/certificate.handler.js +170 -0
  9. package/dist_ts/opsserver/handlers/index.d.ts +1 -0
  10. package/dist_ts/opsserver/handlers/index.js +2 -1
  11. package/dist_ts_interfaces/requests/certificate.d.ts +44 -0
  12. package/dist_ts_interfaces/requests/certificate.js +3 -0
  13. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  14. package/dist_ts_interfaces/requests/index.js +2 -1
  15. package/dist_ts_web/00_commitinfo_data.js +1 -1
  16. package/dist_ts_web/appstate.d.ts +17 -0
  17. package/dist_ts_web/appstate.js +71 -2
  18. package/dist_ts_web/elements/index.d.ts +1 -0
  19. package/dist_ts_web/elements/index.js +2 -1
  20. package/dist_ts_web/elements/ops-dashboard.js +6 -1
  21. package/dist_ts_web/elements/ops-view-certificates.d.ts +20 -0
  22. package/dist_ts_web/elements/ops-view-certificates.js +379 -0
  23. package/dist_ts_web/router.d.ts +1 -1
  24. package/dist_ts_web/router.js +2 -2
  25. package/package.json +2 -2
  26. package/ts/00_commitinfo_data.ts +1 -1
  27. package/ts/classes.dcrouter.ts +63 -10
  28. package/ts/opsserver/classes.opsserver.ts +2 -0
  29. package/ts/opsserver/handlers/certificate.handler.ts +186 -0
  30. package/ts/opsserver/handlers/index.ts +2 -1
  31. package/ts_web/00_commitinfo_data.ts +1 -1
  32. package/ts_web/appstate.ts +98 -2
  33. package/ts_web/elements/index.ts +1 -0
  34. package/ts_web/elements/ops-dashboard.ts +5 -0
  35. package/ts_web/elements/ops-view-certificates.ts +355 -0
  36. package/ts_web/router.ts +1 -1
@@ -0,0 +1,186 @@
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
+ export class CertificateHandler {
6
+ public typedrouter = new plugins.typedrequest.TypedRouter();
7
+
8
+ constructor(private opsServerRef: OpsServer) {
9
+ this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
10
+ this.registerHandlers();
11
+ }
12
+
13
+ private registerHandlers(): void {
14
+ // Get Certificate Overview
15
+ this.typedrouter.addTypedHandler(
16
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificateOverview>(
17
+ 'getCertificateOverview',
18
+ async (dataArg) => {
19
+ const certificates = await this.buildCertificateOverview();
20
+ const summary = this.buildSummary(certificates);
21
+ return { certificates, summary };
22
+ }
23
+ )
24
+ );
25
+
26
+ // Reprovision Certificate
27
+ this.typedrouter.addTypedHandler(
28
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
29
+ 'reprovisionCertificate',
30
+ async (dataArg) => {
31
+ return this.reprovisionCertificate(dataArg.routeName);
32
+ }
33
+ )
34
+ );
35
+ }
36
+
37
+ private async buildCertificateOverview(): Promise<interfaces.requests.ICertificateInfo[]> {
38
+ const dcRouter = this.opsServerRef.dcRouterRef;
39
+ const smartProxy = dcRouter.smartProxy;
40
+ if (!smartProxy) return [];
41
+
42
+ const routes = smartProxy.routeManager.getRoutes();
43
+ const certificates: interfaces.requests.ICertificateInfo[] = [];
44
+
45
+ for (const route of routes) {
46
+ if (!route.name) continue;
47
+
48
+ const tls = route.action?.tls;
49
+ if (!tls) continue;
50
+
51
+ // Skip passthrough routes - they don't manage certificates
52
+ if (tls.mode === 'passthrough') continue;
53
+
54
+ const routeDomains = route.match.domains
55
+ ? (Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains])
56
+ : [];
57
+
58
+ // Determine source
59
+ let source: interfaces.requests.TCertificateSource = 'none';
60
+ if (tls.certificate === 'auto') {
61
+ // Check if a certProvisionFunction is configured
62
+ if ((smartProxy.settings as any).certProvisionFunction) {
63
+ source = 'provision-function';
64
+ } else {
65
+ source = 'acme';
66
+ }
67
+ } else if (tls.certificate && typeof tls.certificate === 'object') {
68
+ source = 'static';
69
+ }
70
+
71
+ // Start with unknown status
72
+ let status: interfaces.requests.TCertificateStatus = 'unknown';
73
+ let expiryDate: string | undefined;
74
+ let issuedAt: string | undefined;
75
+ let issuer: string | undefined;
76
+ let error: string | undefined;
77
+
78
+ // Check event-based status from DcRouter's certificateStatusMap
79
+ const eventStatus = dcRouter.certificateStatusMap.get(route.name);
80
+ if (eventStatus) {
81
+ status = eventStatus.status;
82
+ expiryDate = eventStatus.expiryDate;
83
+ issuedAt = eventStatus.issuedAt;
84
+ error = eventStatus.error;
85
+ if (eventStatus.source) {
86
+ issuer = eventStatus.source;
87
+ }
88
+ }
89
+
90
+ // Try Rust-side certificate status if no event data
91
+ if (status === 'unknown') {
92
+ try {
93
+ const rustStatus = await smartProxy.getCertificateStatus(route.name);
94
+ if (rustStatus) {
95
+ if (rustStatus.expiryDate) expiryDate = rustStatus.expiryDate;
96
+ if (rustStatus.issuer) issuer = rustStatus.issuer;
97
+ if (rustStatus.issuedAt) issuedAt = rustStatus.issuedAt;
98
+ if (rustStatus.status === 'valid' || rustStatus.status === 'expired') {
99
+ status = rustStatus.status;
100
+ }
101
+ }
102
+ } catch {
103
+ // Rust bridge may not support this command yet — ignore
104
+ }
105
+ }
106
+
107
+ // Compute status from expiry date if we have one and status is still valid/unknown
108
+ if (expiryDate && (status === 'valid' || status === 'unknown')) {
109
+ const expiry = new Date(expiryDate);
110
+ const now = new Date();
111
+ const daysUntilExpiry = (expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);
112
+
113
+ if (daysUntilExpiry < 0) {
114
+ status = 'expired';
115
+ } else if (daysUntilExpiry < 30) {
116
+ status = 'expiring';
117
+ } else {
118
+ status = 'valid';
119
+ }
120
+ }
121
+
122
+ // Static certs with no other info default to 'valid'
123
+ if (source === 'static' && status === 'unknown') {
124
+ status = 'valid';
125
+ }
126
+
127
+ const canReprovision = source === 'acme' || source === 'provision-function';
128
+
129
+ certificates.push({
130
+ routeName: route.name,
131
+ domains: routeDomains,
132
+ status,
133
+ source,
134
+ tlsMode: tls.mode as 'terminate' | 'terminate-and-reencrypt' | 'passthrough',
135
+ expiryDate,
136
+ issuer,
137
+ issuedAt,
138
+ error,
139
+ canReprovision,
140
+ });
141
+ }
142
+
143
+ return certificates;
144
+ }
145
+
146
+ private buildSummary(certificates: interfaces.requests.ICertificateInfo[]): {
147
+ total: number;
148
+ valid: number;
149
+ expiring: number;
150
+ expired: number;
151
+ failed: number;
152
+ unknown: number;
153
+ } {
154
+ const summary = { total: 0, valid: 0, expiring: 0, expired: 0, failed: 0, unknown: 0 };
155
+ summary.total = certificates.length;
156
+ for (const cert of certificates) {
157
+ switch (cert.status) {
158
+ case 'valid': summary.valid++; break;
159
+ case 'expiring': summary.expiring++; break;
160
+ case 'expired': summary.expired++; break;
161
+ case 'failed': summary.failed++; break;
162
+ case 'provisioning': // count as unknown
163
+ case 'unknown': summary.unknown++; break;
164
+ }
165
+ }
166
+ return summary;
167
+ }
168
+
169
+ private async reprovisionCertificate(routeName: string): Promise<{ success: boolean; message?: string }> {
170
+ const dcRouter = this.opsServerRef.dcRouterRef;
171
+ const smartProxy = dcRouter.smartProxy;
172
+
173
+ if (!smartProxy) {
174
+ return { success: false, message: 'SmartProxy is not running' };
175
+ }
176
+
177
+ try {
178
+ await smartProxy.provisionCertificate(routeName);
179
+ // Clear event-based status so it gets refreshed
180
+ dcRouter.certificateStatusMap.delete(routeName);
181
+ return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
182
+ } catch (err) {
183
+ return { success: false, message: err.message || 'Failed to reprovision certificate' };
184
+ }
185
+ }
186
+ }
@@ -4,4 +4,5 @@ export * from './logs.handler.js';
4
4
  export * from './security.handler.js';
5
5
  export * from './stats.handler.js';
6
6
  export * from './radius.handler.js';
7
- export * from './email-ops.handler.js';
7
+ export * from './email-ops.handler.js';
8
+ export * from './certificate.handler.js';
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '5.2.0',
6
+ version: '5.4.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -53,6 +53,14 @@ export interface INetworkState {
53
53
  error: string | null;
54
54
  }
55
55
 
56
+ export interface ICertificateState {
57
+ certificates: interfaces.requests.ICertificateInfo[];
58
+ summary: { total: number; valid: number; expiring: number; expired: number; failed: number; unknown: number };
59
+ isLoading: boolean;
60
+ error: string | null;
61
+ lastUpdated: number;
62
+ }
63
+
56
64
  export interface IEmailOpsState {
57
65
  currentView: 'queued' | 'sent' | 'failed' | 'received' | 'security';
58
66
  queuedEmails: interfaces.requests.IEmailQueueItem[];
@@ -103,7 +111,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
103
111
  // Determine initial view from URL path
104
112
  const getInitialView = (): string => {
105
113
  const path = typeof window !== 'undefined' ? window.location.pathname : '/';
106
- const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security'];
114
+ const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates'];
107
115
  const segments = path.split('/').filter(Boolean);
108
116
  const view = segments[0];
109
117
  return validViews.includes(view) ? view : 'overview';
@@ -162,6 +170,18 @@ export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
162
170
  'soft'
163
171
  );
164
172
 
173
+ export const certificateStatePart = await appState.getStatePart<ICertificateState>(
174
+ 'certificates',
175
+ {
176
+ certificates: [],
177
+ summary: { total: 0, valid: 0, expiring: 0, expired: 0, failed: 0, unknown: 0 },
178
+ isLoading: false,
179
+ error: null,
180
+ lastUpdated: 0,
181
+ },
182
+ 'soft'
183
+ );
184
+
165
185
  // Actions for state management
166
186
  interface IActionContext {
167
187
  identity: interfaces.data.IIdentity | null;
@@ -340,7 +360,14 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
340
360
  networkStatePart.dispatchAction(fetchNetworkStatsAction, null);
341
361
  }, 100);
342
362
  }
343
-
363
+
364
+ // If switching to certificates view, ensure we fetch certificate data
365
+ if (viewName === 'certificates' && currentState.activeView !== 'certificates') {
366
+ setTimeout(() => {
367
+ certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
368
+ }, 100);
369
+ }
370
+
344
371
  return {
345
372
  ...currentState,
346
373
  activeView: viewName,
@@ -641,6 +668,66 @@ export const removeFromSuppressionListAction = emailOpsStatePart.createAction<st
641
668
  }
642
669
  );
643
670
 
671
+ // ============================================================================
672
+ // Certificate Actions
673
+ // ============================================================================
674
+
675
+ export const fetchCertificateOverviewAction = certificateStatePart.createAction(async (statePartArg) => {
676
+ const context = getActionContext();
677
+ const currentState = statePartArg.getState();
678
+
679
+ try {
680
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
681
+ interfaces.requests.IReq_GetCertificateOverview
682
+ >('/typedrequest', 'getCertificateOverview');
683
+
684
+ const response = await request.fire({
685
+ identity: context.identity,
686
+ });
687
+
688
+ return {
689
+ certificates: response.certificates,
690
+ summary: response.summary,
691
+ isLoading: false,
692
+ error: null,
693
+ lastUpdated: Date.now(),
694
+ };
695
+ } catch (error) {
696
+ return {
697
+ ...currentState,
698
+ isLoading: false,
699
+ error: error instanceof Error ? error.message : 'Failed to fetch certificate overview',
700
+ };
701
+ }
702
+ });
703
+
704
+ export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
705
+ async (statePartArg, routeName) => {
706
+ const context = getActionContext();
707
+ const currentState = statePartArg.getState();
708
+
709
+ try {
710
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
711
+ interfaces.requests.IReq_ReprovisionCertificate
712
+ >('/typedrequest', 'reprovisionCertificate');
713
+
714
+ await request.fire({
715
+ identity: context.identity,
716
+ routeName,
717
+ });
718
+
719
+ // Re-fetch overview after reprovisioning
720
+ await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
721
+ return statePartArg.getState();
722
+ } catch (error) {
723
+ return {
724
+ ...currentState,
725
+ error: error instanceof Error ? error.message : 'Failed to reprovision certificate',
726
+ };
727
+ }
728
+ }
729
+ );
730
+
644
731
  // Combined refresh action for efficient polling
645
732
  async function dispatchCombinedRefreshAction() {
646
733
  const context = getActionContext();
@@ -725,6 +812,15 @@ async function dispatchCombinedRefreshAction() {
725
812
  });
726
813
  }
727
814
  }
815
+
816
+ // Refresh certificate data if on certificates view
817
+ if (currentView === 'certificates') {
818
+ try {
819
+ await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
820
+ } catch (error) {
821
+ console.error('Certificate refresh failed:', error);
822
+ }
823
+ }
728
824
  } catch (error) {
729
825
  console.error('Combined refresh failed:', error);
730
826
  }
@@ -5,4 +5,5 @@ export * from './ops-view-emails.js';
5
5
  export * from './ops-view-logs.js';
6
6
  export * from './ops-view-config.js';
7
7
  export * from './ops-view-security.js';
8
+ export * from './ops-view-certificates.js';
8
9
  export * from './shared/index.js';
@@ -19,6 +19,7 @@ import { OpsViewEmails } from './ops-view-emails.js';
19
19
  import { OpsViewLogs } from './ops-view-logs.js';
20
20
  import { OpsViewConfig } from './ops-view-config.js';
21
21
  import { OpsViewSecurity } from './ops-view-security.js';
22
+ import { OpsViewCertificates } from './ops-view-certificates.js';
22
23
 
23
24
  @customElement('ops-dashboard')
24
25
  export class OpsDashboard extends DeesElement {
@@ -61,6 +62,10 @@ export class OpsDashboard extends DeesElement {
61
62
  name: 'Security',
62
63
  element: OpsViewSecurity,
63
64
  },
65
+ {
66
+ name: 'Certificates',
67
+ element: OpsViewCertificates,
68
+ },
64
69
  ];
65
70
 
66
71
  /**