@serve.zone/dcrouter 5.5.0 → 6.0.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.
- package/dist_serve/bundle.js +23 -12
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.cert-provision-scheduler.d.ts +60 -0
- package/dist_ts/classes.cert-provision-scheduler.js +141 -0
- package/dist_ts/classes.dcrouter.d.ts +8 -2
- package/dist_ts/classes.dcrouter.js +78 -41
- package/dist_ts/opsserver/handlers/certificate.handler.d.ts +12 -1
- package/dist_ts/opsserver/handlers/certificate.handler.js +119 -35
- package/dist_ts_interfaces/requests/certificate.d.ts +18 -2
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.js +4 -4
- package/dist_ts_web/elements/ops-view-certificates.d.ts +2 -1
- package/dist_ts_web/elements/ops-view-certificates.js +46 -19
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.cert-provision-scheduler.ts +176 -0
- package/ts/classes.dcrouter.ts +81 -41
- package/ts/opsserver/handlers/certificate.handler.ts +137 -33
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +4 -4
- package/ts_web/elements/ops-view-certificates.ts +43 -18
|
@@ -23,24 +23,45 @@ export class CertificateHandler {
|
|
|
23
23
|
)
|
|
24
24
|
);
|
|
25
25
|
|
|
26
|
-
//
|
|
26
|
+
// Legacy route-based reprovision (backward compat)
|
|
27
27
|
this.typedrouter.addTypedHandler(
|
|
28
28
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
|
|
29
29
|
'reprovisionCertificate',
|
|
30
30
|
async (dataArg) => {
|
|
31
|
-
return this.
|
|
31
|
+
return this.reprovisionCertificateByRoute(dataArg.routeName);
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Domain-based reprovision (preferred)
|
|
37
|
+
this.typedrouter.addTypedHandler(
|
|
38
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>(
|
|
39
|
+
'reprovisionCertificateDomain',
|
|
40
|
+
async (dataArg) => {
|
|
41
|
+
return this.reprovisionCertificateDomain(dataArg.domain);
|
|
32
42
|
}
|
|
33
43
|
)
|
|
34
44
|
);
|
|
35
45
|
}
|
|
36
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Build domain-centric certificate overview.
|
|
49
|
+
* Instead of one row per route, we produce one row per unique domain.
|
|
50
|
+
*/
|
|
37
51
|
private async buildCertificateOverview(): Promise<interfaces.requests.ICertificateInfo[]> {
|
|
38
52
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
|
39
53
|
const smartProxy = dcRouter.smartProxy;
|
|
40
54
|
if (!smartProxy) return [];
|
|
41
55
|
|
|
42
56
|
const routes = smartProxy.routeManager.getRoutes();
|
|
43
|
-
|
|
57
|
+
|
|
58
|
+
// Phase 1: Collect unique domains with their associated route info
|
|
59
|
+
const domainMap = new Map<string, {
|
|
60
|
+
routeNames: string[];
|
|
61
|
+
source: interfaces.requests.TCertificateSource;
|
|
62
|
+
tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
|
|
63
|
+
canReprovision: boolean;
|
|
64
|
+
}>();
|
|
44
65
|
|
|
45
66
|
for (const route of routes) {
|
|
46
67
|
if (!route.name) continue;
|
|
@@ -58,7 +79,6 @@ export class CertificateHandler {
|
|
|
58
79
|
// Determine source
|
|
59
80
|
let source: interfaces.requests.TCertificateSource = 'none';
|
|
60
81
|
if (tls.certificate === 'auto') {
|
|
61
|
-
// Check if a certProvisionFunction is configured
|
|
62
82
|
if ((smartProxy.settings as any).certProvisionFunction) {
|
|
63
83
|
source = 'provision-function';
|
|
64
84
|
} else {
|
|
@@ -68,15 +88,44 @@ export class CertificateHandler {
|
|
|
68
88
|
source = 'static';
|
|
69
89
|
}
|
|
70
90
|
|
|
71
|
-
|
|
91
|
+
const canReprovision = source === 'acme' || source === 'provision-function';
|
|
92
|
+
const tlsMode = tls.mode as 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
|
|
93
|
+
|
|
94
|
+
for (const domain of routeDomains) {
|
|
95
|
+
const existing = domainMap.get(domain);
|
|
96
|
+
if (existing) {
|
|
97
|
+
// Add this route name to the existing domain entry
|
|
98
|
+
if (!existing.routeNames.includes(route.name)) {
|
|
99
|
+
existing.routeNames.push(route.name);
|
|
100
|
+
}
|
|
101
|
+
// Upgrade source if more specific
|
|
102
|
+
if (existing.source === 'none' && source !== 'none') {
|
|
103
|
+
existing.source = source;
|
|
104
|
+
existing.canReprovision = canReprovision;
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
domainMap.set(domain, {
|
|
108
|
+
routeNames: [route.name],
|
|
109
|
+
source,
|
|
110
|
+
tlsMode,
|
|
111
|
+
canReprovision,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Phase 2: Resolve status for each unique domain
|
|
118
|
+
const certificates: interfaces.requests.ICertificateInfo[] = [];
|
|
119
|
+
|
|
120
|
+
for (const [domain, info] of domainMap) {
|
|
72
121
|
let status: interfaces.requests.TCertificateStatus = 'unknown';
|
|
73
122
|
let expiryDate: string | undefined;
|
|
74
123
|
let issuedAt: string | undefined;
|
|
75
124
|
let issuer: string | undefined;
|
|
76
125
|
let error: string | undefined;
|
|
77
126
|
|
|
78
|
-
// Check event-based status from
|
|
79
|
-
const eventStatus = dcRouter.certificateStatusMap.get(
|
|
127
|
+
// Check event-based status from certificateStatusMap (now keyed by domain)
|
|
128
|
+
const eventStatus = dcRouter.certificateStatusMap.get(domain);
|
|
80
129
|
if (eventStatus) {
|
|
81
130
|
status = eventStatus.status;
|
|
82
131
|
expiryDate = eventStatus.expiryDate;
|
|
@@ -87,10 +136,10 @@ export class CertificateHandler {
|
|
|
87
136
|
}
|
|
88
137
|
}
|
|
89
138
|
|
|
90
|
-
// Try
|
|
91
|
-
if (status === 'unknown') {
|
|
139
|
+
// Try SmartProxy certificate status if no event data
|
|
140
|
+
if (status === 'unknown' && info.routeNames.length > 0) {
|
|
92
141
|
try {
|
|
93
|
-
const rustStatus = await smartProxy.getCertificateStatus(
|
|
142
|
+
const rustStatus = await smartProxy.getCertificateStatus(info.routeNames[0]);
|
|
94
143
|
if (rustStatus) {
|
|
95
144
|
if (rustStatus.expiryDate) expiryDate = rustStatus.expiryDate;
|
|
96
145
|
if (rustStatus.issuer) issuer = rustStatus.issuer;
|
|
@@ -105,22 +154,19 @@ export class CertificateHandler {
|
|
|
105
154
|
}
|
|
106
155
|
|
|
107
156
|
// Check persisted cert data from StorageManager
|
|
108
|
-
if (status === 'unknown'
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (certData
|
|
114
|
-
|
|
115
|
-
if (certData.created) {
|
|
116
|
-
issuedAt = new Date(certData.created).toISOString();
|
|
117
|
-
}
|
|
118
|
-
issuer = 'smartacme-dns-01';
|
|
157
|
+
if (status === 'unknown') {
|
|
158
|
+
const cleanDomain = domain.replace(/^\*\.?/, '');
|
|
159
|
+
const certData = await dcRouter.storageManager.getJSON(`/certs/${cleanDomain}`);
|
|
160
|
+
if (certData?.validUntil) {
|
|
161
|
+
expiryDate = new Date(certData.validUntil).toISOString();
|
|
162
|
+
if (certData.created) {
|
|
163
|
+
issuedAt = new Date(certData.created).toISOString();
|
|
119
164
|
}
|
|
165
|
+
issuer = 'smartacme-dns-01';
|
|
120
166
|
}
|
|
121
167
|
}
|
|
122
168
|
|
|
123
|
-
// Compute status from expiry date
|
|
169
|
+
// Compute status from expiry date
|
|
124
170
|
if (expiryDate && (status === 'valid' || status === 'unknown')) {
|
|
125
171
|
const expiry = new Date(expiryDate);
|
|
126
172
|
const now = new Date();
|
|
@@ -136,28 +182,36 @@ export class CertificateHandler {
|
|
|
136
182
|
}
|
|
137
183
|
|
|
138
184
|
// Static certs with no other info default to 'valid'
|
|
139
|
-
if (source === 'static' && status === 'unknown') {
|
|
185
|
+
if (info.source === 'static' && status === 'unknown') {
|
|
140
186
|
status = 'valid';
|
|
141
187
|
}
|
|
142
188
|
|
|
143
189
|
// ACME/provision-function routes with no cert data are still provisioning
|
|
144
|
-
if (status === 'unknown' && (source === 'acme' || source === 'provision-function')) {
|
|
190
|
+
if (status === 'unknown' && (info.source === 'acme' || info.source === 'provision-function')) {
|
|
145
191
|
status = 'provisioning';
|
|
146
192
|
}
|
|
147
193
|
|
|
148
|
-
|
|
194
|
+
// Phase 3: Attach backoff info
|
|
195
|
+
let backoffInfo: interfaces.requests.ICertificateInfo['backoffInfo'];
|
|
196
|
+
if (dcRouter.certProvisionScheduler) {
|
|
197
|
+
const bi = await dcRouter.certProvisionScheduler.getBackoffInfo(domain);
|
|
198
|
+
if (bi) {
|
|
199
|
+
backoffInfo = bi;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
149
202
|
|
|
150
203
|
certificates.push({
|
|
151
|
-
|
|
152
|
-
|
|
204
|
+
domain,
|
|
205
|
+
routeNames: info.routeNames,
|
|
153
206
|
status,
|
|
154
|
-
source,
|
|
155
|
-
tlsMode:
|
|
207
|
+
source: info.source,
|
|
208
|
+
tlsMode: info.tlsMode,
|
|
156
209
|
expiryDate,
|
|
157
210
|
issuer,
|
|
158
211
|
issuedAt,
|
|
159
212
|
error,
|
|
160
|
-
canReprovision,
|
|
213
|
+
canReprovision: info.canReprovision,
|
|
214
|
+
backoffInfo,
|
|
161
215
|
});
|
|
162
216
|
}
|
|
163
217
|
|
|
@@ -187,7 +241,10 @@ export class CertificateHandler {
|
|
|
187
241
|
return summary;
|
|
188
242
|
}
|
|
189
243
|
|
|
190
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Legacy route-based reprovisioning
|
|
246
|
+
*/
|
|
247
|
+
private async reprovisionCertificateByRoute(routeName: string): Promise<{ success: boolean; message?: string }> {
|
|
191
248
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
|
192
249
|
const smartProxy = dcRouter.smartProxy;
|
|
193
250
|
|
|
@@ -197,11 +254,58 @@ export class CertificateHandler {
|
|
|
197
254
|
|
|
198
255
|
try {
|
|
199
256
|
await smartProxy.provisionCertificate(routeName);
|
|
200
|
-
// Clear event-based status
|
|
201
|
-
dcRouter.certificateStatusMap
|
|
257
|
+
// Clear event-based status for domains in this route
|
|
258
|
+
for (const [domain, entry] of dcRouter.certificateStatusMap) {
|
|
259
|
+
if (entry.routeNames.includes(routeName)) {
|
|
260
|
+
dcRouter.certificateStatusMap.delete(domain);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
202
263
|
return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
|
|
203
264
|
} catch (err) {
|
|
204
265
|
return { success: false, message: err.message || 'Failed to reprovision certificate' };
|
|
205
266
|
}
|
|
206
267
|
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Domain-based reprovisioning — clears backoff first, then triggers provision
|
|
271
|
+
*/
|
|
272
|
+
private async reprovisionCertificateDomain(domain: string): Promise<{ success: boolean; message?: string }> {
|
|
273
|
+
const dcRouter = this.opsServerRef.dcRouterRef;
|
|
274
|
+
const smartProxy = dcRouter.smartProxy;
|
|
275
|
+
|
|
276
|
+
if (!smartProxy) {
|
|
277
|
+
return { success: false, message: 'SmartProxy is not running' };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Clear backoff for this domain (user override)
|
|
281
|
+
if (dcRouter.certProvisionScheduler) {
|
|
282
|
+
await dcRouter.certProvisionScheduler.clearBackoff(domain);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Clear status map entry so it gets refreshed
|
|
286
|
+
dcRouter.certificateStatusMap.delete(domain);
|
|
287
|
+
|
|
288
|
+
// Try to provision via SmartAcme directly
|
|
289
|
+
if (dcRouter.smartAcme) {
|
|
290
|
+
try {
|
|
291
|
+
await dcRouter.smartAcme.getCertificateForDomain(domain);
|
|
292
|
+
return { success: true, message: `Certificate reprovisioning triggered for domain '${domain}'` };
|
|
293
|
+
} catch (err) {
|
|
294
|
+
return { success: false, message: err.message || `Failed to reprovision certificate for ${domain}` };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Fallback: try provisioning via the first matching route
|
|
299
|
+
const routeNames = dcRouter.findRouteNamesForDomain(domain);
|
|
300
|
+
if (routeNames.length > 0) {
|
|
301
|
+
try {
|
|
302
|
+
await smartProxy.provisionCertificate(routeNames[0]);
|
|
303
|
+
return { success: true, message: `Certificate reprovisioning triggered for domain '${domain}' via route '${routeNames[0]}'` };
|
|
304
|
+
} catch (err) {
|
|
305
|
+
return { success: false, message: err.message || `Failed to reprovision certificate for ${domain}` };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return { success: false, message: `No routes found for domain '${domain}'` };
|
|
310
|
+
}
|
|
207
311
|
}
|
package/ts_web/appstate.ts
CHANGED
|
@@ -719,18 +719,18 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
|
|
|
719
719
|
});
|
|
720
720
|
|
|
721
721
|
export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
|
|
722
|
-
async (statePartArg,
|
|
722
|
+
async (statePartArg, domain) => {
|
|
723
723
|
const context = getActionContext();
|
|
724
724
|
const currentState = statePartArg.getState();
|
|
725
725
|
|
|
726
726
|
try {
|
|
727
727
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
728
|
-
interfaces.requests.
|
|
729
|
-
>('/typedrequest', '
|
|
728
|
+
interfaces.requests.IReq_ReprovisionCertificateDomain
|
|
729
|
+
>('/typedrequest', 'reprovisionCertificateDomain');
|
|
730
730
|
|
|
731
731
|
await request.fire({
|
|
732
732
|
identity: context.identity,
|
|
733
|
-
|
|
733
|
+
domain,
|
|
734
734
|
});
|
|
735
735
|
|
|
736
736
|
// Re-fetch overview after reprovisioning
|
|
@@ -94,13 +94,13 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
94
94
|
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
.
|
|
97
|
+
.routePills {
|
|
98
98
|
display: flex;
|
|
99
99
|
flex-wrap: wrap;
|
|
100
100
|
gap: 4px;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
.
|
|
103
|
+
.routePill {
|
|
104
104
|
display: inline-flex;
|
|
105
105
|
align-items: center;
|
|
106
106
|
padding: 2px 8px;
|
|
@@ -125,6 +125,17 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
125
125
|
white-space: nowrap;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
.backoffIndicator {
|
|
129
|
+
display: inline-flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
gap: 4px;
|
|
132
|
+
font-size: 11px;
|
|
133
|
+
color: ${cssManager.bdTheme('#9a3412', '#fb923c')};
|
|
134
|
+
padding: 2px 6px;
|
|
135
|
+
border-radius: 4px;
|
|
136
|
+
background: ${cssManager.bdTheme('#fff7ed', '#431407')};
|
|
137
|
+
}
|
|
138
|
+
|
|
128
139
|
.expiryInfo {
|
|
129
140
|
font-size: 12px;
|
|
130
141
|
}
|
|
@@ -218,14 +229,16 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
218
229
|
<dees-table
|
|
219
230
|
.data=${this.certState.certificates}
|
|
220
231
|
.displayFunction=${(cert: interfaces.requests.ICertificateInfo) => ({
|
|
221
|
-
|
|
222
|
-
|
|
232
|
+
Domain: cert.domain,
|
|
233
|
+
Routes: this.renderRoutePills(cert.routeNames),
|
|
223
234
|
Status: this.renderStatusBadge(cert.status),
|
|
224
235
|
Source: this.renderSourceBadge(cert.source),
|
|
225
236
|
Expires: this.renderExpiry(cert.expiryDate),
|
|
226
|
-
Error: cert.
|
|
227
|
-
? html`<span class="
|
|
228
|
-
:
|
|
237
|
+
Error: cert.backoffInfo
|
|
238
|
+
? html`<span class="backoffIndicator">${cert.backoffInfo.failures} failures, retry ${this.formatRetryTime(cert.backoffInfo.retryAfter)}</span>`
|
|
239
|
+
: cert.error
|
|
240
|
+
? html`<span class="errorText" title="${cert.error}">${cert.error}</span>`
|
|
241
|
+
: '',
|
|
229
242
|
})}
|
|
230
243
|
.dataActions=${[
|
|
231
244
|
{
|
|
@@ -245,11 +258,11 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
245
258
|
}
|
|
246
259
|
await appstate.certificateStatePart.dispatchAction(
|
|
247
260
|
appstate.reprovisionCertificateAction,
|
|
248
|
-
cert.
|
|
261
|
+
cert.domain,
|
|
249
262
|
);
|
|
250
263
|
const { DeesToast } = await import('@design.estate/dees-catalog');
|
|
251
264
|
DeesToast.show({
|
|
252
|
-
message: `Reprovisioning triggered for ${cert.
|
|
265
|
+
message: `Reprovisioning triggered for ${cert.domain}`,
|
|
253
266
|
type: 'success',
|
|
254
267
|
duration: 3000,
|
|
255
268
|
});
|
|
@@ -263,7 +276,7 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
263
276
|
const cert = actionData.item;
|
|
264
277
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
265
278
|
await DeesModal.createAndShow({
|
|
266
|
-
heading: `Certificate: ${cert.
|
|
279
|
+
heading: `Certificate: ${cert.domain}`,
|
|
267
280
|
content: html`
|
|
268
281
|
<div style="padding: 20px;">
|
|
269
282
|
<dees-dataview-codebox
|
|
@@ -275,10 +288,10 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
275
288
|
`,
|
|
276
289
|
menuOptions: [
|
|
277
290
|
{
|
|
278
|
-
name: 'Copy
|
|
291
|
+
name: 'Copy Domain',
|
|
279
292
|
iconName: 'copy',
|
|
280
293
|
action: async () => {
|
|
281
|
-
await navigator.clipboard.writeText(cert.
|
|
294
|
+
await navigator.clipboard.writeText(cert.domain);
|
|
282
295
|
},
|
|
283
296
|
},
|
|
284
297
|
],
|
|
@@ -287,7 +300,7 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
287
300
|
},
|
|
288
301
|
]}
|
|
289
302
|
heading1="Certificate Status"
|
|
290
|
-
heading2="TLS certificates
|
|
303
|
+
heading2="TLS certificates by domain"
|
|
291
304
|
searchable
|
|
292
305
|
.pagination=${true}
|
|
293
306
|
.paginationSize=${50}
|
|
@@ -296,14 +309,14 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
296
309
|
`;
|
|
297
310
|
}
|
|
298
311
|
|
|
299
|
-
private
|
|
312
|
+
private renderRoutePills(routeNames: string[]): TemplateResult {
|
|
300
313
|
const maxShow = 3;
|
|
301
|
-
const visible =
|
|
302
|
-
const remaining =
|
|
314
|
+
const visible = routeNames.slice(0, maxShow);
|
|
315
|
+
const remaining = routeNames.length - maxShow;
|
|
303
316
|
|
|
304
317
|
return html`
|
|
305
|
-
<span class="
|
|
306
|
-
${visible.map((
|
|
318
|
+
<span class="routePills">
|
|
319
|
+
${visible.map((r) => html`<span class="routePill">${r}</span>`)}
|
|
307
320
|
${remaining > 0 ? html`<span class="moreCount">+${remaining} more</span>` : ''}
|
|
308
321
|
</span>
|
|
309
322
|
`;
|
|
@@ -352,4 +365,16 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
352
365
|
</span>
|
|
353
366
|
`;
|
|
354
367
|
}
|
|
368
|
+
|
|
369
|
+
private formatRetryTime(retryAfter?: string): string {
|
|
370
|
+
if (!retryAfter) return 'soon';
|
|
371
|
+
const retryDate = new Date(retryAfter);
|
|
372
|
+
const now = new Date();
|
|
373
|
+
const diffMs = retryDate.getTime() - now.getTime();
|
|
374
|
+
if (diffMs <= 0) return 'now';
|
|
375
|
+
const diffMin = Math.ceil(diffMs / 60000);
|
|
376
|
+
if (diffMin < 60) return `in ${diffMin}m`;
|
|
377
|
+
const diffHours = Math.ceil(diffMin / 60);
|
|
378
|
+
return `in ${diffHours}h`;
|
|
379
|
+
}
|
|
355
380
|
}
|